Let developers override the whole transport

The developer setting that overrode the LiveKit service URL becomes a
JSON transport, so that a `url`-only or combined transport can be tested
against a homeserver that does not advertise one yet. The settings modal
shows each LiveKit room's role instead of a never-populated isLocal flag.

Folds "Avoid null in transport dev setting" from the same PR. The setting feeds the SDK's transport option; the config fallback becomes a transport the host builds.

Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
Robin
2026-10-14 10:23:29 +02:00
committed by Timo
co-authored by Johannes Marbach
parent f97dd2adab
commit 289260531b
3 changed files with 36 additions and 29 deletions
+6 -3
View File
@@ -83,7 +83,7 @@ import {
cameraCodec, cameraCodec,
cameraFramerate, cameraFramerate,
cameraResolution, cameraResolution,
customLivekitUrl, customTransport,
echoCancellationSetting, echoCancellationSetting,
matrixRTCMode as matrixRTCModeSetting, matrixRTCMode as matrixRTCModeSetting,
noiseSuppressionSetting, noiseSuppressionSetting,
@@ -823,7 +823,10 @@ export function rtcParticipationOptions(
delegatedDelayedLeave: session.delegated_delayed_leave, delegatedDelayedLeave: session.delegated_delayed_leave,
}, },
mediaQuality: config.media_quality, mediaQuality: config.media_quality,
transportUrl: customLivekitUrl.value$.value ?? undefined, transport: customTransport.value$.value ?? undefined,
fallbackTransportUrl: config.livekit?.livekit_service_url, fallbackTransport: config.livekit && {
type: "livekit",
livekit_service_url: config.livekit.livekit_service_url,
},
}; };
} }
+8 -9
View File
@@ -29,7 +29,7 @@ import {
MatrixRTCMode, MatrixRTCMode,
} from "@element-hq/matrixrtc-sdk"; } from "@element-hq/matrixrtc-sdk";
import type * as MatrixRTCSdk from "@element-hq/matrixrtc-sdk"; import type * as MatrixRTCSdk from "@element-hq/matrixrtc-sdk";
import { mockConfig } from "../utils/test"; import { exampleTransport, mockConfig } from "../utils/test";
// Mock url params hook to avoid environment-dependent snapshot churn. // Mock url params hook to avoid environment-dependent snapshot churn.
vi.mock("../UrlParams", () => ({ vi.mock("../UrlParams", () => ({
@@ -232,10 +232,12 @@ describe("DeveloperSettingsTab", () => {
const saveButton = screen.getByRole("button", { name: "Save" }); const saveButton = screen.getByRole("button", { name: "Save" });
await user.click(saveButton); await user.click(saveButton);
expect(authenticateWithTransport).toHaveBeenCalledWith( expect(authenticateWithTransport).toHaveBeenCalledWith(
expect.anything(), expect.objectContaining({
expect.anything(), transport: { type: "livekit", url: "wss://example.livekit.valid" },
"wss://example.livekit.valid", serverName: "hs",
"#testRoom", roomId: "#testRoom",
role: "publisher",
}),
); );
expect(customTransportSetting.getValue()).toEqual({ expect(customTransportSetting.getValue()).toEqual({
@@ -266,7 +268,7 @@ describe("DeveloperSettingsTab", () => {
const saveButton = screen.getByRole("button", { name: "Save" }); const saveButton = screen.getByRole("button", { name: "Save" });
(authenticateWithTransport as Mock).mockImplementation(() => { (authenticateWithTransport as Mock).mockImplementation(() => {
throw new Error("Invalid URL"); throw new Error("Failed to get SFU config");
}); });
await user.click(saveButton); await user.click(saveButton);
expect( expect(
@@ -276,9 +278,6 @@ describe("DeveloperSettingsTab", () => {
}); });
}); });
// Add this test inside the describe("DeveloperSettingsTab", () => { block,
// after the custom transport tests:
describe("enable extended livekit logs", () => { describe("enable extended livekit logs", () => {
afterEach(() => { afterEach(() => {
enableExtendedLivekitLogsSetting.setValue(false); enableExtendedLivekitLogsSetting.setValue(false);
+22 -17
View File
@@ -20,6 +20,7 @@ import {
type MatrixClient, type MatrixClient,
} from "matrix-js-sdk"; } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { import {
EditInPlace, EditInPlace,
ErrorMessage, ErrorMessage,
@@ -173,17 +174,17 @@ export const DeveloperSettingsTab: FC<Props> = ({
enableExtendedLivekitLogsSetting, enableExtendedLivekitLogsSetting,
); );
const [customTransportUpdateError, setCustomLivekitUrlUpdateError] = useState< const [customTransportUpdateError, setCustomTransportUpdateError] = useState<
string | null string | null
>(null); >(null);
const [customTransport, setCustomTransport] = useSetting( const [customTransport, setCustomTransport] = useSetting(
customTransportSetting, customTransportSetting,
); );
const [customTransportTextBuffer, setCustomLivekitUrlTextBuffer] = useState( const [customTransportTextBuffer, setCustomTransportTextBuffer] = useState(
() => transportText(customTransport), () => transportText(customTransport),
); );
useEffect(() => { useEffect(() => {
setCustomLivekitUrlTextBuffer(transportText(customTransport)); setCustomTransportTextBuffer(transportText(customTransport));
}, [customTransport]); }, [customTransport]);
const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting); const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting);
@@ -499,11 +500,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
cancelButtonLabel={t("developer_mode.custom_transport.reset")} cancelButtonLabel={t("developer_mode.custom_transport.reset")}
onSave={useCallback( onSave={useCallback(
async (e: React.FormEvent<HTMLFormElement>): Promise<void> => { async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
if ( if (roomId === undefined || customTransportTextBuffer === "") {
roomId === undefined ||
customTransportTextBuffer === "" ||
customTransportTextBuffer === null
) {
setCustomTransport(null); setCustomTransport(null);
return; return;
} }
@@ -511,36 +508,39 @@ export const DeveloperSettingsTab: FC<Props> = ({
try { try {
const userId = client.getUserId(); const userId = client.getUserId();
const deviceId = client.getDeviceId(); const deviceId = client.getDeviceId();
const serverName = client.getDomain();
if (userId === null || deviceId === null) { if (userId === null || deviceId === null || serverName === null) {
throw new Error("Invalid user or device ID"); throw new Error("Invalid user, device ID or server name");
} }
await authenticateWithTransport( const transport = JSON.parse(customTransportTextBuffer);
// Validate the transport by attempting to get a token for it
await authenticateWithTransport({
client, client,
membership: { userId, deviceId, memberId: "" }, membership: { userId, deviceId, memberId: "" },
transport, transport,
serverName: client.getDomain()!, serverName,
roomId, roomId,
slotId: "org.example.fake-slot#room", slotId: "org.example.fake-slot#room",
role: "publisher", role: "publisher",
logger, logger,
}); });
setCustomLivekitUrlUpdateError(null); setCustomTransportUpdateError(null);
setCustomTransport(transport); setCustomTransport(transport);
} catch { } catch {
setCustomLivekitUrlUpdateError( setCustomTransportUpdateError(
"invalid transport (did not update)", "invalid transport (did not update)",
); );
} }
}, },
[customTransportTextBuffer, setCustomTransport, client, roomId], [customTransportTextBuffer, setCustomTransport, client, roomId],
)} )}
value={customTransportTextBuffer ?? ""} value={customTransportTextBuffer}
onChange={useCallback( onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => { (event: ChangeEvent<HTMLInputElement>): void => {
setCustomLivekitUrlTextBuffer(event.target.value); setCustomTransportTextBuffer(event.target.value);
}, },
[setCustomLivekitUrlTextBuffer], [setCustomTransportTextBuffer],
)} )}
onCancel={useCallback( onCancel={useCallback(
(e: React.FormEvent<HTMLFormElement>) => { (e: React.FormEvent<HTMLFormElement>) => {
@@ -659,6 +659,11 @@ export const DeveloperSettingsTab: FC<Props> = ({
); );
}; };
/** The text shown in the custom transport field for a given setting value. */
function transportText(transport: UnstableLivekitTransport | null): string {
return transport === null ? "" : JSON.stringify(transport);
}
const TransportBox: FC<TransportInfo> = ({ transport, local }) => { const TransportBox: FC<TransportInfo> = ({ transport, local }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const resolved = useBehavior(transport.resolved$); const resolved = useBehavior(transport.resolved$);