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,
cameraFramerate,
cameraResolution,
customLivekitUrl,
customTransport,
echoCancellationSetting,
matrixRTCMode as matrixRTCModeSetting,
noiseSuppressionSetting,
@@ -823,7 +823,10 @@ export function rtcParticipationOptions(
delegatedDelayedLeave: session.delegated_delayed_leave,
},
mediaQuality: config.media_quality,
transportUrl: customLivekitUrl.value$.value ?? undefined,
fallbackTransportUrl: config.livekit?.livekit_service_url,
transport: customTransport.value$.value ?? undefined,
fallbackTransport: config.livekit && {
type: "livekit",
livekit_service_url: config.livekit.livekit_service_url,
},
};
}
+8 -9
View File
@@ -29,7 +29,7 @@ import {
MatrixRTCMode,
} 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.
vi.mock("../UrlParams", () => ({
@@ -232,10 +232,12 @@ describe("DeveloperSettingsTab", () => {
const saveButton = screen.getByRole("button", { name: "Save" });
await user.click(saveButton);
expect(authenticateWithTransport).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
"wss://example.livekit.valid",
"#testRoom",
expect.objectContaining({
transport: { type: "livekit", url: "wss://example.livekit.valid" },
serverName: "hs",
roomId: "#testRoom",
role: "publisher",
}),
);
expect(customTransportSetting.getValue()).toEqual({
@@ -266,7 +268,7 @@ describe("DeveloperSettingsTab", () => {
const saveButton = screen.getByRole("button", { name: "Save" });
(authenticateWithTransport as Mock).mockImplementation(() => {
throw new Error("Invalid URL");
throw new Error("Failed to get SFU config");
});
await user.click(saveButton);
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", () => {
afterEach(() => {
enableExtendedLivekitLogsSetting.setValue(false);
+22 -17
View File
@@ -20,6 +20,7 @@ import {
type MatrixClient,
} from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import {
EditInPlace,
ErrorMessage,
@@ -173,17 +174,17 @@ export const DeveloperSettingsTab: FC<Props> = ({
enableExtendedLivekitLogsSetting,
);
const [customTransportUpdateError, setCustomLivekitUrlUpdateError] = useState<
const [customTransportUpdateError, setCustomTransportUpdateError] = useState<
string | null
>(null);
const [customTransport, setCustomTransport] = useSetting(
customTransportSetting,
);
const [customTransportTextBuffer, setCustomLivekitUrlTextBuffer] = useState(
const [customTransportTextBuffer, setCustomTransportTextBuffer] = useState(
() => transportText(customTransport),
);
useEffect(() => {
setCustomLivekitUrlTextBuffer(transportText(customTransport));
setCustomTransportTextBuffer(transportText(customTransport));
}, [customTransport]);
const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting);
@@ -499,11 +500,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
cancelButtonLabel={t("developer_mode.custom_transport.reset")}
onSave={useCallback(
async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
if (
roomId === undefined ||
customTransportTextBuffer === "" ||
customTransportTextBuffer === null
) {
if (roomId === undefined || customTransportTextBuffer === "") {
setCustomTransport(null);
return;
}
@@ -511,36 +508,39 @@ export const DeveloperSettingsTab: FC<Props> = ({
try {
const userId = client.getUserId();
const deviceId = client.getDeviceId();
const serverName = client.getDomain();
if (userId === null || deviceId === null) {
throw new Error("Invalid user or device ID");
if (userId === null || deviceId === null || serverName === null) {
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,
membership: { userId, deviceId, memberId: "" },
transport,
serverName: client.getDomain()!,
serverName,
roomId,
slotId: "org.example.fake-slot#room",
role: "publisher",
logger,
});
setCustomLivekitUrlUpdateError(null);
setCustomTransportUpdateError(null);
setCustomTransport(transport);
} catch {
setCustomLivekitUrlUpdateError(
setCustomTransportUpdateError(
"invalid transport (did not update)",
);
}
},
[customTransportTextBuffer, setCustomTransport, client, roomId],
)}
value={customTransportTextBuffer ?? ""}
value={customTransportTextBuffer}
onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
setCustomLivekitUrlTextBuffer(event.target.value);
setCustomTransportTextBuffer(event.target.value);
},
[setCustomLivekitUrlTextBuffer],
[setCustomTransportTextBuffer],
)}
onCancel={useCallback(
(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 { t } = useTranslation();
const resolved = useBehavior(transport.resolved$);