Add matrix_rtc_mode config option (#4014)

* Move MatrixRTCMode enum from settings.ts to ConfigOptions.ts

* Add matrix_rtc_mode config option

* add matrix_rtc_mode to config.sample.json

* Update src/settings/DeveloperSettingsTab.tsx

Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Update src/settings/DeveloperSettingsTab.test.tsx

Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* reviewer comments

---------

Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
fkwp
2026-06-05 11:49:43 +02:00
committed by GitHub
parent 3a6579f78d
commit b99c8821d3
14 changed files with 236 additions and 26 deletions

View File

@@ -12,6 +12,7 @@
"feature_use_device_session_member_events": true
},
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
"matrix_rtc_mode": "legacy",
"matrix_rtc_session": {
"wait_for_key_rotation_ms": 3000,
"membership_event_expiry_ms": 180000000,

54
src/config/Config.test.ts Normal file
View File

@@ -0,0 +1,54 @@
/*
Copyright 2026 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 { describe, expect, it, vi, afterEach } from "vitest";
import { logger } from "matrix-js-sdk/lib/logger";
import { validateConfig } from "./Config";
import { MatrixRTCMode } from "./ConfigOptions";
describe("validateConfig", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("passes through a missing matrix_rtc_mode unchanged", () => {
const result = validateConfig({});
expect(result.matrix_rtc_mode).toBeUndefined();
});
it.each(Object.values(MatrixRTCMode))(
"keeps a valid matrix_rtc_mode value (%s)",
(mode) => {
const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
const result = validateConfig({ matrix_rtc_mode: mode });
expect(result.matrix_rtc_mode).toBe(mode);
expect(warnSpy).not.toHaveBeenCalled();
},
);
it("drops an invalid matrix_rtc_mode value and warns", () => {
const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
const result = validateConfig({
// Intentionally bypass the type to simulate bad JSON.
matrix_rtc_mode: "nonsense" as unknown as MatrixRTCMode,
});
expect(result.matrix_rtc_mode).toBeUndefined();
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toContain("nonsense");
});
it("does not touch unrelated fields when dropping an invalid mode", () => {
vi.spyOn(logger, "warn").mockImplementation(() => {});
const result = validateConfig({
matrix_rtc_mode: "nope" as unknown as MatrixRTCMode,
ssla: "https://example.invalid/ssla",
});
expect(result.matrix_rtc_mode).toBeUndefined();
expect(result.ssla).toBe("https://example.invalid/ssla");
});
});

View File

@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { merge } from "lodash-es";
import { logger } from "matrix-js-sdk/lib/logger";
import { getUrlParams } from "../UrlParams";
import {
@@ -14,6 +15,11 @@ import {
type ResolvedConfigOptions,
} from "./ConfigOptions";
import { isFailure } from "../utils/fetch";
import { MatrixRTCMode } from "./ConfigOptions";
const VALID_MATRIX_RTC_MODES: ReadonlySet<string> = new Set(
Object.values(MatrixRTCMode),
);
export class Config {
private static internalInstance: Config | undefined;
@@ -44,7 +50,11 @@ export class Config {
Config.internalInstance.initPromise = downloadConfig(fetchTarget).then(
(config) => {
internalInstance.config = merge({}, DEFAULT_CONFIG, config);
internalInstance.config = merge(
{},
DEFAULT_CONFIG,
validateConfig(config),
);
},
);
}
@@ -84,6 +94,17 @@ export class Config {
private initPromise?: Promise<void>;
}
export function validateConfig(config: ConfigOptions): ConfigOptions {
const mode = config.matrix_rtc_mode;
if (mode !== undefined && !VALID_MATRIX_RTC_MODES.has(mode)) {
logger.warn(
`Ignoring invalid matrix_rtc_mode in config.json: ${String(mode)}`,
);
delete config.matrix_rtc_mode;
}
return config;
}
async function downloadConfig(fetchTarget: string): Promise<ConfigOptions> {
const response = await fetch(fetchTarget);

View File

@@ -6,6 +6,26 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
/**
* The MatrixRTC mode determines how Element Call interacts with the
* MatrixRTC backend and other participants. Selectable via the Developer
* Settings, or pinned for a deployment via `matrix_rtc_mode` in config.json.
*/
export enum MatrixRTCMode {
/** Legacy single-SFU + user-keyed memberships + legacy JWT endpoint. */
Legacy = "legacy",
/** Multi-SFU transport, legacy JWT endpoint, no sticky events. */
Compatibility = "compatibility",
/**
* Multi-SFU transport with:
* - sticky events
* - hashed RTC backend identity
* - the new endpoint for the jwt token on the local membership (remote memberships will always try the new jwt endpoint first -> then the legacy one)
* - use the hashed identity for the local membership
*/
Matrix_2_0 = "matrix_2_0",
}
export interface ConfigOptions {
/**
* The Posthog endpoint to which analytics data will be sent.
@@ -104,6 +124,13 @@ export interface ConfigOptions {
*/
sync_disconnect_grace_period_ms?: number;
/**
* Pins the {@link MatrixRTCMode} for all clients on this deployment,
* overriding any per-user choice from the Developer Settings. If unset,
* the user's Developer Settings choice (or its default of `Legacy`) wins.
*/
matrix_rtc_mode?: MatrixRTCMode;
/**
* These are low level options that are used to configure the MatrixRTC session.
* Take care when changing these options.

View File

@@ -17,7 +17,10 @@ import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
import {
customLivekitUrl as customLivekitUrlSetting,
enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting,
matrixRTCMode as matrixRTCModeSetting,
} from "./settings";
import { MatrixRTCMode } from "../config/ConfigOptions";
import { mockConfig } from "../utils/test";
// Mock url params hook to avoid environment-dependent snapshot churn.
vi.mock("../UrlParams", () => ({
@@ -311,4 +314,102 @@ describe("DeveloperSettingsTab", () => {
expect(enableExtendedLivekitLogsSetting.getValue()).toBe(true);
});
});
describe("matrix rtc mode", () => {
afterEach(() => {
matrixRTCModeSetting.setValue(MatrixRTCMode.Legacy);
vi.restoreAllMocks();
});
function getModeRadios(): {
legacy: HTMLInputElement;
compatibility: HTMLInputElement;
matrix20: HTMLInputElement;
} {
return {
legacy: screen.getByDisplayValue(
MatrixRTCMode.Legacy,
) as HTMLInputElement,
compatibility: screen.getByDisplayValue(
MatrixRTCMode.Compatibility,
) as HTMLInputElement,
matrix20: screen.getByDisplayValue(
MatrixRTCMode.Matrix_2_0,
) as HTMLInputElement,
};
}
it("radios reflect the localStorage setting when config does not force the mode", async () => {
mockConfig({});
matrixRTCModeSetting.setValue(MatrixRTCMode.Compatibility);
const client = createMockMatrixClient();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
/>
</TooltipProvider>,
);
await waitFor(() =>
expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(),
);
const radios = getModeRadios();
expect(radios.compatibility).toBeChecked();
expect(radios.legacy).not.toBeChecked();
expect(radios.matrix20).not.toBeChecked();
// None are disabled by config; only Matrix_2_0 may be disabled by sticky-events support.
expect(radios.legacy).not.toBeDisabled();
expect(radios.compatibility).not.toBeDisabled();
});
it.each([
MatrixRTCMode.Legacy,
MatrixRTCMode.Compatibility,
MatrixRTCMode.Matrix_2_0,
])(
"disables all radios and shows the config value (%s) as checked when matrix_rtc_mode is set",
async (configMode) => {
mockConfig({ matrix_rtc_mode: configMode });
// Local setting is intentionally different from the config value to
// prove config wins.
matrixRTCModeSetting.setValue(
configMode === MatrixRTCMode.Legacy
? MatrixRTCMode.Compatibility
: MatrixRTCMode.Legacy,
);
const client = createMockMatrixClient();
render(
<TooltipProvider>
<DeveloperSettingsTab
client={client}
env={{} as unknown as ImportMetaEnv}
/>
</TooltipProvider>,
);
await waitFor(() =>
expect(client.doesServerSupportUnstableFeature).toHaveBeenCalled(),
);
const radios = getModeRadios();
expect(radios.legacy).toBeDisabled();
expect(radios.compatibility).toBeDisabled();
expect(radios.matrix20).toBeDisabled();
const checkedValue = (
{
[MatrixRTCMode.Legacy]: radios.legacy,
[MatrixRTCMode.Compatibility]: radios.compatibility,
[MatrixRTCMode.Matrix_2_0]: radios.matrix20,
} as const
)[configMode];
expect(checkedValue).toBeChecked();
},
);
});
});

View File

@@ -33,6 +33,7 @@ import {
import { type Room as LivekitRoom } from "livekit-client";
import { FieldRow, InputField } from "../input/Input";
import { Config } from "../config/Config";
import {
useSetting,
duplicateTiles as duplicateTilesSetting,
@@ -42,9 +43,9 @@ import {
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
matrixRTCMode as matrixRTCModeSetting,
customLivekitUrl as customLivekitUrlSetting,
MatrixRTCMode,
enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting,
} from "./settings";
import { MatrixRTCMode } from "../config/ConfigOptions";
import styles from "./DeveloperSettingsTab.module.css";
import { useUrlParams } from "../UrlParams";
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
@@ -93,6 +94,11 @@ export const DeveloperSettingsTab: FC<Props> = ({
},
[setMatrixRTCMode],
);
const configMatrixRTCMode = Config.get().matrix_rtc_mode as
| MatrixRTCMode
| undefined;
const matrixRTCModeForced = configMatrixRTCMode !== undefined;
const effectiveMatrixRTCMode = configMatrixRTCMode ?? matrixRTCMode;
const [showConnectionStats, setShowConnectionStats] = useSetting(
showConnectionStatsSetting,
@@ -312,13 +318,15 @@ export const DeveloperSettingsTab: FC<Props> = ({
<Heading as="h3" type="body" weight="semibold" size="lg">
{t("developer_mode.matrixRTCMode.title")}
</Heading>
{matrixRTCModeForced && <p>Your deployment overrides the mode.</p>}
<Form>
<InlineField
name={matrixRTCModeRadioGroup}
control={
<RadioControl
checked={matrixRTCMode === MatrixRTCMode.Legacy}
checked={effectiveMatrixRTCMode === MatrixRTCMode.Legacy}
value={MatrixRTCMode.Legacy}
disabled={matrixRTCModeForced}
onChange={onMatrixRTCModeChange}
/>
}
@@ -332,8 +340,9 @@ export const DeveloperSettingsTab: FC<Props> = ({
name={matrixRTCModeRadioGroup}
control={
<RadioControl
checked={matrixRTCMode === MatrixRTCMode.Compatibility}
checked={effectiveMatrixRTCMode === MatrixRTCMode.Compatibility}
value={MatrixRTCMode.Compatibility}
disabled={matrixRTCModeForced}
onChange={onMatrixRTCModeChange}
/>
}
@@ -347,9 +356,9 @@ export const DeveloperSettingsTab: FC<Props> = ({
name={matrixRTCModeRadioGroup}
control={
<RadioControl
checked={matrixRTCMode === MatrixRTCMode.Matrix_2_0}
checked={effectiveMatrixRTCMode === MatrixRTCMode.Matrix_2_0}
value={MatrixRTCMode.Matrix_2_0}
disabled={!stickyEventsSupported}
disabled={matrixRTCModeForced || !stickyEventsSupported}
onChange={onMatrixRTCModeChange}
/>
}

View File

@@ -11,6 +11,7 @@ import { BehaviorSubject } from "rxjs";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { type Behavior } from "../state/Behavior";
import { useBehavior } from "../useBehavior";
import { MatrixRTCMode } from "../config/ConfigOptions";
export class Setting<T> {
public constructor(
@@ -134,18 +135,6 @@ export const enableExtendedLivekitLogs = new Setting<boolean>(
false,
);
export enum MatrixRTCMode {
Legacy = "legacy",
Compatibility = "compatibility",
/** This implies using
* - sticky events
* - hashed RTC backend identity
* - the new endpoint for the jwt token on the local membership (remote memberships will always try the new jwt endpoint first -> then the legacy one)
* - use the hashed identity for the local membership
*/
Matrix_2_0 = "matrix_2_0",
}
export const matrixRTCMode = new Setting<MatrixRTCMode>(
"matrix-rtc-mode",
MatrixRTCMode.Legacy,

View File

@@ -65,7 +65,7 @@ import {
localParticipant,
withCallViewModel as withCallViewModelInMode,
} from "./CallViewModelTestUtils.ts";
import { MatrixRTCMode } from "../../settings/settings.ts";
import { MatrixRTCMode } from "../../config/ConfigOptions.ts";
import { initializeWidget } from "../../widget.ts";
initializeWidget();

View File

@@ -60,10 +60,11 @@ import {
} from "../../utils/observable";
import {
duplicateTiles,
MatrixRTCMode,
playReactionsSound,
showReactions,
} from "../../settings/settings";
import { Config } from "../../config/Config";
import { MatrixRTCMode } from "../../config/ConfigOptions";
import { isFirefox, platform } from "../../Platform";
import { setPipEnabled$ } from "../../controls";
import { TileStore } from "../TileStore";
@@ -416,8 +417,15 @@ export function createCallViewModel$(
options.encryptionSystem,
matrixRTCSession,
);
// matrix_rtc_mode in config.json overrides the user's Developer Settings choice.
// It is validated at config load (src/config/Config.ts) so the cast is safe.
const configMatrixRTCMode = Config.get().matrix_rtc_mode as
| MatrixRTCMode
| undefined;
const matrixRTCMode$ =
options.matrixRTCMode$ ?? constant(MatrixRTCMode.Legacy);
configMatrixRTCMode !== undefined
? constant(configMatrixRTCMode)
: (options.matrixRTCMode$ ?? constant(MatrixRTCMode.Legacy));
// Each hbar seperates a block of input variables required for the CallViewModel to function.
// The outputs of this block is written under the hbar.

View File

@@ -56,7 +56,7 @@ import {
import { type Behavior, constant } from "../Behavior";
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
import { type MediaDevices } from "../MediaDevices";
import { type MatrixRTCMode } from "../../settings/settings";
import { type MatrixRTCMode } from "../../config/ConfigOptions";
mockConfig({
livekit: { livekit_service_url: "http://my-default-service-url.com" },

View File

@@ -26,7 +26,7 @@ import { logger } from "matrix-js-sdk/lib/logger";
import { type LocalParticipant, type LocalTrack } from "livekit-client";
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics";
import { MatrixRTCMode } from "../../../settings/settings";
import { MatrixRTCMode } from "../../../config/ConfigOptions";
import { type HomeserverDisconnectReason } from "./HomeserverConnected";
import {
flushPromises,

View File

@@ -53,7 +53,7 @@ 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 } from "../../../config/ConfigOptions.ts";
import { Config } from "../../../config/Config.ts";
import {
ConnectionState,

View File

@@ -15,7 +15,7 @@ import { constant } from "./Behavior.ts";
import { aliceParticipant, localRtcMember } from "../utils/test-fixtures.ts";
import { ElementWidgetActions, widget } from "../widget.ts";
import { E2eeType } from "../e2ee/e2eeType.ts";
import { MatrixRTCMode } from "../settings/settings.ts";
import { MatrixRTCMode } from "../config/ConfigOptions.ts";
vi.mock("@livekit/components-core", { spy: true });

View File

@@ -38,7 +38,7 @@ import { type MediaDevices } from "../state/MediaDevices";
import { aliceRtcMember, localRtcMember } from "./test-fixtures";
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
import { constant } from "../state/Behavior";
import { MatrixRTCMode } from "../settings/settings";
import { MatrixRTCMode } from "../config/ConfigOptions";
import { createCallFooterViewModel } from "../components/CallFooterViewModel";
import { type FooterSnapshot } from "../components/CallFooter";
import { type ViewModel } from "../state/ViewModel";