mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
feat: camera quality settings, audio processing toggles, config-seeded defaults
- Add camera video quality controls (resolution/framerate/bitrate/codec) to Settings > Video, mirroring the screen share settings UI - Add audio processing toggles (echo cancellation, noise suppression, auto gain control) to Settings > Audio, replacing URL-param-only controls - Display raw values inline on all sliders (framerate, bitrate, volume) - Add config-seeded defaults: config.json media_quality values now seed Setting defaults for users who haven't explicitly set preferences - Camera settings are applied when joining a call via ConnectionFactory Signed-off-by: Ryan Emmick <ryanemmick4@gmail.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
|
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { getUrlParams } from "./UrlParams";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
|
import { seedSettingsFromConfig } from "./settings/settings";
|
||||||
import { platform } from "./Platform";
|
import { platform } from "./Platform";
|
||||||
import { isFailure } from "./utils/fetch";
|
import { isFailure } from "./utils/fetch";
|
||||||
import { initializeWidget } from "./widget";
|
import { initializeWidget } from "./widget";
|
||||||
@@ -220,6 +221,7 @@ export class Initializer {
|
|||||||
this.loadStates.config = LoadState.Loading;
|
this.loadStates.config = LoadState.Loading;
|
||||||
Config.init().then(
|
Config.init().then(
|
||||||
() => {
|
() => {
|
||||||
|
seedSettingsFromConfig(Config.get().media_quality);
|
||||||
this.loadStates.config = LoadState.Loaded;
|
this.loadStates.config = LoadState.Loaded;
|
||||||
this.initStep(resolve);
|
this.initStep(resolve);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,3 +52,8 @@ Please see LICENSE in the repository root for full details.
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--cpd-color-text-link-external);
|
border-color: var(--cpd-color-text-link-external);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settingValue {
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ import {
|
|||||||
screenShareFramerate as screenShareFramerateSetting,
|
screenShareFramerate as screenShareFramerateSetting,
|
||||||
screenShareBitrate as screenShareBitrateSetting,
|
screenShareBitrate as screenShareBitrateSetting,
|
||||||
screenShareCodec as screenShareCodecSetting,
|
screenShareCodec as screenShareCodecSetting,
|
||||||
|
advancedCamera as advancedCameraSetting,
|
||||||
|
cameraResolution as cameraResolutionSetting,
|
||||||
|
cameraFramerate as cameraFramerateSetting,
|
||||||
|
cameraBitrate as cameraBitrateSetting,
|
||||||
|
cameraCodec as cameraCodecSetting,
|
||||||
|
echoCancellationSetting,
|
||||||
|
noiseSuppressionSetting,
|
||||||
|
autoGainControlSetting,
|
||||||
type VideoCodec,
|
type VideoCodec,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||||
@@ -157,6 +165,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
<div className={styles.volumeSlider}>
|
<div className={styles.volumeSlider}>
|
||||||
<label>
|
<label>
|
||||||
{t("settings.screen_share_framerate_label", "Framerate")}
|
{t("settings.screen_share_framerate_label", "Framerate")}
|
||||||
|
{": "}
|
||||||
|
<span className={styles.settingValue}>{framerateRaw} fps</span>
|
||||||
</label>
|
</label>
|
||||||
<Slider
|
<Slider
|
||||||
label={t("settings.screen_share_framerate_label", "Framerate")}
|
label={t("settings.screen_share_framerate_label", "Framerate")}
|
||||||
@@ -172,6 +182,10 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
<div className={styles.volumeSlider}>
|
<div className={styles.volumeSlider}>
|
||||||
<label>
|
<label>
|
||||||
{t("settings.screen_share_bitrate_label", "Bitrate")}
|
{t("settings.screen_share_bitrate_label", "Bitrate")}
|
||||||
|
{": "}
|
||||||
|
<span className={styles.settingValue}>
|
||||||
|
{(bitrateRaw / 1_000_000).toFixed(1)} Mbps
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Slider
|
<Slider
|
||||||
label={t("settings.screen_share_bitrate_label", "Bitrate")}
|
label={t("settings.screen_share_bitrate_label", "Bitrate")}
|
||||||
@@ -209,6 +223,177 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CameraSettings: React.FC = (): ReactNode => {
|
||||||
|
const [advancedEnabled, setAdvancedEnabled] = useSetting(
|
||||||
|
advancedCameraSetting,
|
||||||
|
);
|
||||||
|
const [resolution, setResolution] = useSetting(cameraResolutionSetting);
|
||||||
|
const [framerate, setFramerate] = useSetting(cameraFramerateSetting);
|
||||||
|
const [framerateRaw, setFramerateRaw] = useState(framerate);
|
||||||
|
const [bitrate, setBitrate] = useSetting(cameraBitrateSetting);
|
||||||
|
const [bitrateRaw, setBitrateRaw] = useState(bitrate);
|
||||||
|
const [codec, setCodec] = useSetting(cameraCodecSetting);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>{t("settings.camera_header", "Camera quality")}</h4>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="advancedCamera"
|
||||||
|
label={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.",
|
||||||
|
)}
|
||||||
|
type="checkbox"
|
||||||
|
checked={advancedEnabled}
|
||||||
|
onChange={(e): void => setAdvancedEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{advancedEnabled && (
|
||||||
|
<>
|
||||||
|
<div className={styles.volumeSlider}>
|
||||||
|
<label htmlFor="cameraResolution">
|
||||||
|
{t("settings.camera_resolution_label", "Resolution")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="cameraResolution"
|
||||||
|
value={resolution}
|
||||||
|
onChange={(e): void => setResolution(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="640x360">360p</option>
|
||||||
|
<option value="960x540">540p</option>
|
||||||
|
<option value="1280x720">720p</option>
|
||||||
|
<option value="1920x1080">1080p</option>
|
||||||
|
<option value="2560x1440">1440p</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={styles.volumeSlider}>
|
||||||
|
<label>
|
||||||
|
{t("settings.camera_framerate_label", "Framerate")}
|
||||||
|
{": "}
|
||||||
|
<span className={styles.settingValue}>
|
||||||
|
{framerateRaw} fps
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
label={t("settings.camera_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.camera_bitrate_label", "Bitrate")}
|
||||||
|
{": "}
|
||||||
|
<span className={styles.settingValue}>
|
||||||
|
{(bitrateRaw / 1_000_000).toFixed(1)} Mbps
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
label={t("settings.camera_bitrate_label", "Bitrate")}
|
||||||
|
value={bitrateRaw}
|
||||||
|
onValueChange={setBitrateRaw}
|
||||||
|
onValueCommit={setBitrate}
|
||||||
|
min={200_000}
|
||||||
|
max={8_000_000}
|
||||||
|
step={100_000}
|
||||||
|
tooltipFormatter={(v): string =>
|
||||||
|
`${(v / 1_000_000).toFixed(1)} Mbps`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.volumeSlider}>
|
||||||
|
<label htmlFor="cameraCodec">
|
||||||
|
{t("settings.camera_codec_label", "Codec")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="cameraCodec"
|
||||||
|
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();
|
||||||
@@ -263,7 +448,13 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.volumeSlider}>
|
<div className={styles.volumeSlider}>
|
||||||
<label>{t("settings.audio_tab.effect_volume_label")}</label>
|
<label>
|
||||||
|
{t("settings.audio_tab.effect_volume_label")}
|
||||||
|
{": "}
|
||||||
|
<span className={styles.settingValue}>
|
||||||
|
{Math.round(soundVolumeRaw * 100)}%
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<p>{t("settings.audio_tab.effect_volume_description")}</p>
|
<p>{t("settings.audio_tab.effect_volume_description")}</p>
|
||||||
<Slider
|
<Slider
|
||||||
label={t("video_tile.volume")}
|
label={t("video_tile.volume")}
|
||||||
@@ -276,6 +467,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
<Separator />
|
||||||
|
<AudioProcessingSettings />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -295,6 +488,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
<Separator />
|
<Separator />
|
||||||
<BlurCheckbox />
|
<BlurCheckbox />
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<CameraSettings />
|
||||||
|
<Separator />
|
||||||
<ScreenShareSettings />
|
<ScreenShareSettings />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class Setting<T> {
|
|||||||
this.key = `matrix-setting-${key}`;
|
this.key = `matrix-setting-${key}`;
|
||||||
|
|
||||||
const storedValue = localStorage.getItem(this.key);
|
const storedValue = localStorage.getItem(this.key);
|
||||||
|
this.hasStoredValue = storedValue !== null;
|
||||||
let initialValue = defaultValue;
|
let initialValue = defaultValue;
|
||||||
if (storedValue !== null) {
|
if (storedValue !== null) {
|
||||||
try {
|
try {
|
||||||
@@ -39,6 +40,7 @@ export class Setting<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly key: string;
|
private readonly key: string;
|
||||||
|
private readonly hasStoredValue: boolean;
|
||||||
|
|
||||||
private readonly _value$: BehaviorSubject<T>;
|
private readonly _value$: BehaviorSubject<T>;
|
||||||
private readonly _lastUpdateReason$: BehaviorSubject<string | null>;
|
private readonly _lastUpdateReason$: BehaviorSubject<string | null>;
|
||||||
@@ -53,6 +55,17 @@ export class Setting<T> {
|
|||||||
public readonly getValue = (): T => {
|
public readonly getValue = (): T => {
|
||||||
return this._value$.getValue();
|
return this._value$.getValue();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the setting's value from a config source, but only if the user
|
||||||
|
* hasn't explicitly set a value in localStorage. This lets admins set
|
||||||
|
* org-wide defaults in config.json that users can override.
|
||||||
|
*/
|
||||||
|
public seedFromConfig(value: T): void {
|
||||||
|
if (!this.hasStoredValue) {
|
||||||
|
this._value$.next(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,3 +190,93 @@ export const screenShareCodec = new Setting<VideoCodec>(
|
|||||||
"screen-share-codec",
|
"screen-share-codec",
|
||||||
"vp9",
|
"vp9",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Camera video quality settings
|
||||||
|
export const advancedCamera = new Setting<boolean>("advanced-camera", false);
|
||||||
|
|
||||||
|
export const cameraResolution = new Setting<string>(
|
||||||
|
"camera-resolution",
|
||||||
|
"1280x720",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const cameraFramerate = new Setting<number>("camera-framerate", 30);
|
||||||
|
|
||||||
|
export const cameraBitrate = new Setting<number>(
|
||||||
|
"camera-bitrate",
|
||||||
|
1_700_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const cameraCodec = new Setting<VideoCodec>("camera-codec", "vp8");
|
||||||
|
|
||||||
|
// Audio processing settings
|
||||||
|
export const echoCancellationSetting = new Setting<boolean>(
|
||||||
|
"echo-cancellation",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const noiseSuppressionSetting = new Setting<boolean>(
|
||||||
|
"noise-suppression",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const autoGainControlSetting = new Setting<boolean>(
|
||||||
|
"auto-gain-control",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed setting defaults from config.json's media_quality section.
|
||||||
|
* Call this after Config.init() has resolved.
|
||||||
|
* 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,
|
||||||
|
): void {
|
||||||
|
if (!mediaQuality) return;
|
||||||
|
|
||||||
|
const codec = mediaQuality.video_codec;
|
||||||
|
if (codec) {
|
||||||
|
screenShareCodec.seedFromConfig(codec);
|
||||||
|
cameraCodec.seedFromConfig(codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
const screen = mediaQuality.screen_share;
|
||||||
|
if (screen) {
|
||||||
|
if (screen.max_resolution) {
|
||||||
|
const width = Math.round((screen.max_resolution * 16) / 9);
|
||||||
|
screenShareResolution.seedFromConfig(`${width}x${screen.max_resolution}`);
|
||||||
|
}
|
||||||
|
if (screen.max_framerate) {
|
||||||
|
screenShareFramerate.seedFromConfig(screen.max_framerate);
|
||||||
|
}
|
||||||
|
if (screen.max_bitrate) {
|
||||||
|
screenShareBitrate.seedFromConfig(screen.max_bitrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = mediaQuality.video;
|
||||||
|
if (video) {
|
||||||
|
if (video.max_resolution) {
|
||||||
|
const width = Math.round((video.max_resolution * 16) / 9);
|
||||||
|
cameraResolution.seedFromConfig(`${width}x${video.max_resolution}`);
|
||||||
|
}
|
||||||
|
if (video.max_framerate) {
|
||||||
|
cameraFramerate.seedFromConfig(video.max_framerate);
|
||||||
|
}
|
||||||
|
if (video.max_bitrate) {
|
||||||
|
cameraBitrate.seedFromConfig(video.max_bitrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -488,8 +488,6 @@ export function createCallViewModel$(
|
|||||||
livekitKeyProvider,
|
livekitKeyProvider,
|
||||||
getUrlParams().controlledAudioDevices,
|
getUrlParams().controlledAudioDevices,
|
||||||
options.livekitRoomFactory,
|
options.livekitRoomFactory,
|
||||||
getUrlParams().echoCancellation,
|
|
||||||
getUrlParams().noiseSuppression,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectionManager = createConnectionManager$({
|
const connectionManager = createConnectionManager$({
|
||||||
|
|||||||
@@ -27,10 +27,17 @@ import type {
|
|||||||
import type { MediaDevices } from "../../MediaDevices.ts";
|
import type { MediaDevices } from "../../MediaDevices.ts";
|
||||||
import type { Behavior } from "../../Behavior.ts";
|
import type { Behavior } from "../../Behavior.ts";
|
||||||
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||||
|
import { getLiveKitOptions } from "../../../livekit/options.ts";
|
||||||
import {
|
import {
|
||||||
defaultLiveKitOptions,
|
advancedCamera,
|
||||||
getLiveKitOptions,
|
cameraResolution,
|
||||||
} from "../../../livekit/options.ts";
|
cameraFramerate,
|
||||||
|
cameraBitrate,
|
||||||
|
cameraCodec,
|
||||||
|
echoCancellationSetting,
|
||||||
|
noiseSuppressionSetting,
|
||||||
|
autoGainControlSetting,
|
||||||
|
} from "../../../settings/settings.ts";
|
||||||
|
|
||||||
// TODO evaluate if this should be done like the Publisher Factory
|
// TODO evaluate if this should be done like the Publisher Factory
|
||||||
export interface ConnectionFactory {
|
export interface ConnectionFactory {
|
||||||
@@ -56,8 +63,6 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
* @param livekitKeyProvider - Optional key provider for end-to-end encryption.
|
* @param livekitKeyProvider - Optional key provider for end-to-end encryption.
|
||||||
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
|
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
|
||||||
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
||||||
* @param echoCancellation - Whether to enable echo cancellation for audio capture.
|
|
||||||
* @param noiseSuppression - Whether to enable noise suppression for audio capture.
|
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private client: OpenIDClientParts,
|
private client: OpenIDClientParts,
|
||||||
@@ -67,8 +72,6 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
livekitKeyProvider: BaseKeyProvider | undefined,
|
livekitKeyProvider: BaseKeyProvider | undefined,
|
||||||
private controlledAudioDevices: boolean,
|
private controlledAudioDevices: boolean,
|
||||||
livekitRoomFactory?: () => LivekitRoom,
|
livekitRoomFactory?: () => LivekitRoom,
|
||||||
echoCancellation: boolean = true,
|
|
||||||
noiseSuppression: boolean = true,
|
|
||||||
) {
|
) {
|
||||||
const defaultFactory = (): LivekitRoom =>
|
const defaultFactory = (): LivekitRoom =>
|
||||||
new LivekitRoom(
|
new LivekitRoom(
|
||||||
@@ -82,8 +85,6 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
worker: new E2EEWorker(),
|
worker: new E2EEWorker(),
|
||||||
},
|
},
|
||||||
controlledAudioDevices: this.controlledAudioDevices,
|
controlledAudioDevices: this.controlledAudioDevices,
|
||||||
echoCancellation,
|
|
||||||
noiseSuppression,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
|
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
|
||||||
@@ -122,14 +123,13 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
||||||
|
* Reads audio processing and camera quality settings directly from Settings.
|
||||||
*/
|
*/
|
||||||
function generateRoomOption({
|
function generateRoomOption({
|
||||||
devices,
|
devices,
|
||||||
processorState,
|
processorState,
|
||||||
e2eeLivekitOptions,
|
e2eeLivekitOptions,
|
||||||
controlledAudioDevices,
|
controlledAudioDevices,
|
||||||
echoCancellation,
|
|
||||||
noiseSuppression,
|
|
||||||
}: {
|
}: {
|
||||||
devices: MediaDevices;
|
devices: MediaDevices;
|
||||||
processorState: ProcessorState;
|
processorState: ProcessorState;
|
||||||
@@ -138,22 +138,46 @@ function generateRoomOption({
|
|||||||
| { e2eeManager: BaseE2EEManager }
|
| { e2eeManager: BaseE2EEManager }
|
||||||
| undefined;
|
| undefined;
|
||||||
controlledAudioDevices: boolean;
|
controlledAudioDevices: boolean;
|
||||||
echoCancellation: boolean;
|
|
||||||
noiseSuppression: boolean;
|
|
||||||
}): RoomOptions {
|
}): RoomOptions {
|
||||||
const liveKitOptions = getLiveKitOptions();
|
const liveKitOptions = getLiveKitOptions();
|
||||||
|
|
||||||
|
// Apply advanced camera settings if enabled
|
||||||
|
let videoCaptureDefaults = {
|
||||||
|
...liveKitOptions.videoCaptureDefaults,
|
||||||
|
deviceId: devices.videoInput.selected$.value?.id,
|
||||||
|
processor: processorState.processor,
|
||||||
|
};
|
||||||
|
let publishDefaults = liveKitOptions.publishDefaults;
|
||||||
|
|
||||||
|
if (advancedCamera.getValue()) {
|
||||||
|
const resParts = cameraResolution.getValue().split("x");
|
||||||
|
const width = Number(resParts[0]);
|
||||||
|
const height = Number(resParts[1]);
|
||||||
|
const fps = cameraFramerate.getValue();
|
||||||
|
const bps = cameraBitrate.getValue();
|
||||||
|
const codec = cameraCodec.getValue();
|
||||||
|
|
||||||
|
videoCaptureDefaults = {
|
||||||
|
...videoCaptureDefaults,
|
||||||
|
resolution: { width, height, frameRate: fps },
|
||||||
|
};
|
||||||
|
publishDefaults = {
|
||||||
|
...publishDefaults,
|
||||||
|
videoEncoding: { maxBitrate: bps, maxFramerate: fps },
|
||||||
|
videoCodec: codec,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...liveKitOptions,
|
...liveKitOptions,
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults,
|
||||||
...liveKitOptions.videoCaptureDefaults,
|
publishDefaults,
|
||||||
deviceId: devices.videoInput.selected$.value?.id,
|
|
||||||
processor: processorState.processor,
|
|
||||||
},
|
|
||||||
audioCaptureDefaults: {
|
audioCaptureDefaults: {
|
||||||
...liveKitOptions.audioCaptureDefaults,
|
...liveKitOptions.audioCaptureDefaults,
|
||||||
deviceId: devices.audioInput.selected$.value?.id,
|
deviceId: devices.audioInput.selected$.value?.id,
|
||||||
echoCancellation,
|
echoCancellation: echoCancellationSetting.getValue(),
|
||||||
noiseSuppression,
|
noiseSuppression: noiseSuppressionSetting.getValue(),
|
||||||
|
autoGainControl: autoGainControlSetting.getValue(),
|
||||||
},
|
},
|
||||||
audioOutput: {
|
audioOutput: {
|
||||||
// When using controlled audio devices, we don't want to set the
|
// When using controlled audio devices, we don't want to set the
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ import {
|
|||||||
} from "../../../utils/test.ts";
|
} from "../../../utils/test.ts";
|
||||||
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||||
import { constant } from "../../Behavior";
|
import { constant } from "../../Behavior";
|
||||||
|
import {
|
||||||
|
echoCancellationSetting,
|
||||||
|
noiseSuppressionSetting,
|
||||||
|
} from "../../../settings/settings.ts";
|
||||||
|
|
||||||
// At the top of your test file, after imports
|
// At the top of your test file, after imports
|
||||||
vi.mock("livekit-client", async (importOriginal) => {
|
vi.mock("livekit-client", async (importOriginal) => {
|
||||||
@@ -58,11 +62,14 @@ describe("ECConnectionFactory - Audio inputs options", () => {
|
|||||||
{ echo: false, noise: true },
|
{ echo: false, noise: true },
|
||||||
{ echo: false, noise: false },
|
{ echo: false, noise: false },
|
||||||
])(
|
])(
|
||||||
"it sets echoCancellation=$echo and noiseSuppression=$noise based on constructor parameters",
|
"it sets echoCancellation=$echo and noiseSuppression=$noise based on settings",
|
||||||
({ echo, noise }) => {
|
({ echo, noise }) => {
|
||||||
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
|
|
||||||
const RoomConstructor = vi.mocked(LivekitRoom);
|
const RoomConstructor = vi.mocked(LivekitRoom);
|
||||||
|
|
||||||
|
// Set audio processing settings
|
||||||
|
echoCancellationSetting.setValue(echo);
|
||||||
|
noiseSuppressionSetting.setValue(noise);
|
||||||
|
|
||||||
const ecConnectionFactory = new ECConnectionFactory(
|
const ecConnectionFactory = new ECConnectionFactory(
|
||||||
mockClient,
|
mockClient,
|
||||||
"!roomid:example.org",
|
"!roomid:example.org",
|
||||||
@@ -73,9 +80,6 @@ describe("ECConnectionFactory - Audio inputs options", () => {
|
|||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
false,
|
false,
|
||||||
undefined,
|
|
||||||
echo,
|
|
||||||
noise,
|
|
||||||
);
|
);
|
||||||
ecConnectionFactory.createConnection(
|
ecConnectionFactory.createConnection(
|
||||||
testScope,
|
testScope,
|
||||||
@@ -120,9 +124,6 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
|||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
controlled,
|
controlled,
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
ecConnectionFactory.createConnection(
|
ecConnectionFactory.createConnection(
|
||||||
testScope,
|
testScope,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const createRoomWidgetClientSpy = vi.mocked(createRoomWidgetClient);
|
|||||||
vi.mock("./config/Config", () => ({
|
vi.mock("./config/Config", () => ({
|
||||||
Config: {
|
Config: {
|
||||||
init: vi.fn().mockImplementation(async () => Promise.resolve()),
|
init: vi.fn().mockImplementation(async () => Promise.resolve()),
|
||||||
|
get: vi.fn().mockReturnValue({}),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const configInitSpy = vi.mocked(Config.init);
|
const configInitSpy = vi.mocked(Config.init);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { IWidgetApiRequest } from "matrix-widget-api";
|
|||||||
import { LazyEventEmitter } from "./LazyEventEmitter";
|
import { LazyEventEmitter } from "./LazyEventEmitter";
|
||||||
import { getUrlParams } from "./UrlParams";
|
import { getUrlParams } from "./UrlParams";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
|
import { seedSettingsFromConfig } from "./settings/settings";
|
||||||
import { ElementCallReactionEventType } from "./reactions";
|
import { ElementCallReactionEventType } from "./reactions";
|
||||||
|
|
||||||
// Subset of the actions in element-web
|
// Subset of the actions in element-web
|
||||||
@@ -195,6 +196,7 @@ export const initializeWidget = (
|
|||||||
// Wait for the config file to be ready (we load very early on so it might not
|
// Wait for the config file to be ready (we load very early on so it might not
|
||||||
// be otherwise)
|
// be otherwise)
|
||||||
await Config.init();
|
await Config.init();
|
||||||
|
seedSettingsFromConfig(Config.get().media_quality);
|
||||||
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
|
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user