From e6fed4c4585c81edd69c0f107e52001e36f048b7 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 7 Oct 2026 14:45:19 +0200 Subject: [PATCH] WIP --- locales/en/app.json | 6 +- package.json | 2 +- pnpm-lock.yaml | 12 +- src/livekit/auth/csApi.ts | 14 ++- src/livekit/auth/index.ts | 6 +- src/livekit/auth/openID.test.ts | 114 ++++++----------- src/livekit/auth/openID.ts | 53 ++++---- src/room/InCallView.tsx | 22 ++-- src/settings/DeveloperSettingsTab.test.tsx | 30 ++--- src/settings/DeveloperSettingsTab.tsx | 76 ++++++----- src/settings/SettingsModal.tsx | 2 +- src/settings/settings.ts | 5 +- src/state/CallViewModel/CallViewModel.ts | 19 +-- .../CallViewModel/CallViewModelTestUtils.ts | 10 +- .../localMember/LocalMember.test.ts | 40 +++--- .../localMember/LocalTransport.test.ts | 119 ++++++------------ .../localMember/LocalTransport.ts | 13 +- .../remoteMembers/Connection.test.ts | 74 +++++------ .../CallViewModel/remoteMembers/Connection.ts | 5 +- .../remoteMembers/ConnectionManager.test.ts | 17 ++- .../remoteMembers/ECConnectionFactory.test.ts | 14 ++- .../remoteMembers/integration.test.ts | 13 +- src/utils/test.ts | 16 +-- 23 files changed, 312 insertions(+), 370 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index e31c7d50a..5be1bfff6 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -59,10 +59,10 @@ "developer_mode": { "always_show_iphone_earpiece": "Show iPhone earpiece option on all platforms", "crypto_version": "Crypto version: {{version}}", - "custom_livekit_url": { - "current_url": "Currently set to: ", + "custom_transport": { + "current": "Currently set to: ", "from_config": "Currently, no overwrite is set. Url from config is used.", - "label": "Custom Livekit-url", + "label": "Custom transport JSON", "reset": "Reset overwrite", "save": "Save", "saving": "Saving..." diff --git a/package.json b/package.json index 0a76e0c01..8e679de3d 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "lodash-es": "^4.17.21", "loglevel": "^1.9.1", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robin/livekit-updates", - "matrix-widget-api": "^1.18.0", + "matrix-widget-api": "^1.20.0", "node-stdlib-browser": "^1.3.1", "normalize.css": "^8.0.1", "oxfmt": "^0.66.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9968c64a8..57d341b54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,8 +194,8 @@ importers: specifier: github:matrix-org/matrix-js-sdk#robin/livekit-updates version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/621b225e227dc6dc2b1c9d53413235d8999e6d54 matrix-widget-api: - specifier: ^1.18.0 - version: 1.19.0 + specifier: ^1.20.0 + version: 1.20.0 node-stdlib-browser: specifier: ^1.3.1 version: 1.3.1 @@ -4678,9 +4678,6 @@ packages: version: 42.3.0 engines: {node: '>=22.0.0'} - matrix-widget-api@1.19.0: - resolution: {integrity: sha512-Qe7L6G1mQJphBC47/Bzp5ljPIK1UARgLiuGgS+/KmEs/eaNHkQ3+ArD0/0kvMV+d6kCGAZ9OKRqsnuPjizI+tw==} - matrix-widget-api@1.20.0: resolution: {integrity: sha512-qwFP37rgns7mPesM5/vvZgknzb+kvyRB5BISv5sCuBxsGMceMr+Kogf7OkjRxBXdMCPBFuf6gIuYRe5S+lpzQw==} @@ -10244,11 +10241,6 @@ snapshots: sdp-transform: 3.0.0 unhomoglyph: 1.0.6 - matrix-widget-api@1.19.0: - dependencies: - '@types/events': 3.0.3 - events: 3.3.0 - matrix-widget-api@1.20.0: dependencies: '@types/events': 3.0.3 diff --git a/src/livekit/auth/csApi.ts b/src/livekit/auth/csApi.ts index ec82490b7..e8ebe68f5 100644 --- a/src/livekit/auth/csApi.ts +++ b/src/livekit/auth/csApi.ts @@ -20,17 +20,19 @@ export type ClientGetTokenParts = Pick< "_unstable_getLivekitToken" >; +export interface GetSFUConfigParams { + client: ClientGetTokenParts; + membership: CallMembershipIdentityParts; + url: string; + roomId: string; +} + export async function getSFUConfig({ client, membership, url, roomId, -}: { - client: ClientGetTokenParts; - membership: CallMembershipIdentityParts; - url: string; - roomId: string; -}): Promise { +}: GetSFUConfigParams): Promise { const res = await doNetworkOperationWithRetry(async () => client._unstable_getLivekitToken({ url, diff --git a/src/livekit/auth/index.ts b/src/livekit/auth/index.ts index 511c8e004..86e0fb87f 100644 --- a/src/livekit/auth/index.ts +++ b/src/livekit/auth/index.ts @@ -15,9 +15,9 @@ import * as csApi from "./csApi"; import { type SFUConfig } from "./types"; import { NoMatrix2AuthorizationService } from "../../utils/errors"; -export { ClientOpenIDParts } from "./openID"; -export { ClientGetTokenParts, ClientDelegationParts } from "./csApi"; -export * from "./types"; +export type { ClientOpenIDParts } from "./openID"; +export type { ClientGetTokenParts, ClientDelegationParts } from "./csApi"; +export type * from "./types"; /** * Checks whether an error means "this homeserver does not implement the diff --git a/src/livekit/auth/openID.test.ts b/src/livekit/auth/openID.test.ts index 2b500fc4f..5e44fc3f6 100644 --- a/src/livekit/auth/openID.test.ts +++ b/src/livekit/auth/openID.test.ts @@ -17,21 +17,40 @@ import { import fetchMock from "fetch-mock"; import { MatrixError } from "matrix-js-sdk"; -import { getSFUConfigWithOpenID, type ClientOpenIDParts } from "./openIDSFU"; -import { testJWTToken } from "../utils/test-fixtures"; -import { ownMemberMock } from "../utils/test"; -import { FailToGetOpenIdToken } from "../utils/errors"; +import { + getSFUConfig, + type GetSFUConfigParams, + type ClientOpenIDParts, +} from "./openID"; +import { testJWTToken } from "../../utils/test-fixtures"; +import { ownMemberMock } from "../../utils/test"; +import { FailToGetOpenIdToken } from "../../utils/errors"; +import { logger } from "matrix-js-sdk/lib/logger"; -const sfuUrl = "https://sfu.example.org"; +const serviceUrl = "https://sfu.example.org"; -describe("getSFUConfigWithOpenID", () => { +const delayParams = { + delayEndpointBaseUrl: "https://matrix.homeserverserver.org", + delayId: "mock_delay_id", +}; + +describe("getSFUConfig", () => { let matrixClient: MockedObject; + let params: GetSFUConfigParams; beforeEach(() => { fetchMock.catch(404); matrixClient = { getOpenIdToken: vitest.fn(), getDeviceId: vitest.fn(), }; + params = { + client: matrixClient, + membership: ownMemberMock, + serviceUrl: "https://sfu.example.org", + roomId: "!example_room_id", + role: "subscriber", + logger, + }; }); afterEach(() => { vitest.clearAllMocks(); @@ -42,18 +61,13 @@ describe("getSFUConfigWithOpenID", () => { fetchMock.post("https://sfu.example.org/sfu/get", () => { return { status: 200, - body: { url: sfuUrl, jwt: testJWTToken }, + body: { url: serviceUrl, jwt: testJWTToken }, }; }); - const config = await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - ); + const config = await getSFUConfig(params); expect(config).toEqual({ jwt: testJWTToken, - url: sfuUrl, + url: serviceUrl, livekitIdentity: "@me:example.org:ABCDEF", livekitAlias: "!example_room_id", }); @@ -71,12 +85,7 @@ describe("getSFUConfigWithOpenID", () => { }; }); try { - await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - ); + await getSFUConfig(params); } catch (ex: unknown) { expect(ex).toBeInstanceOf(FailToGetOpenIdToken); expect((ex as FailToGetOpenIdToken).cause).toBeInstanceOf(MatrixError); @@ -116,23 +125,14 @@ describe("getSFUConfigWithOpenID", () => { return { status: 200, - body: { url: sfuUrl, jwt: testJWTToken }, + body: { url: serviceUrl, jwt: testJWTToken }, }; }, { overwriteRoutes: true }, ); - // Note: Assuming getSFUConfigWithOpenID eventually calls getLiveKitJWT - const config = await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - { - delayEndpointBaseUrl: "https://matrix.homeserverserver.org", - delayId: "mock_delay_id", - }, - ); + // Note: Assuming getSFUConfig eventually calls getLiveKitJWT + const config = await getSFUConfig({ ...params, ...delayParams }); expect(config.jwt).toBe(testJWTToken); expect(callCount).toBe(2); @@ -153,7 +153,7 @@ describe("getSFUConfigWithOpenID", () => { ) { return { status: 200, - body: { url: sfuUrl, jwt: testJWTToken }, + body: { url: serviceUrl, jwt: testJWTToken }, }; } return { @@ -164,21 +164,12 @@ describe("getSFUConfigWithOpenID", () => { { overwriteRoutes: true }, ); - const config = await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - { - delayEndpointBaseUrl: "https://homeserverserver.org/cs_api", - delayId: "mock_delay_id", - }, - ); + const config = await getSFUConfig({ ...params, ...delayParams }); // Prüfe das Ergebnis expect(config).toMatchObject({ jwt: testJWTToken, - url: sfuUrl, + url: serviceUrl, }); void (await fetchMock.flush()); @@ -204,16 +195,7 @@ describe("getSFUConfigWithOpenID", () => { }; }); try { - await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - { - delayEndpointBaseUrl: "https://matrix.homeserverserver.org", - delayId: "mock_delay_id", - }, - ); + await getSFUConfig({ ...params, ...delayParams }); } catch (ex) { expect(ex).toBeInstanceOf(FailToGetOpenIdToken); expect((ex as FailToGetOpenIdToken).cause).toBeInstanceOf(MatrixError); @@ -251,7 +233,7 @@ describe("getSFUConfigWithOpenID", () => { fetchMock.post("https://sfu.example.org/get_token", () => { return { status: 200, - body: { url: sfuUrl, jwt: testJWTToken }, + body: { url: serviceUrl, jwt: testJWTToken }, }; }); fetchMock.post("https://sfu.example.org/sfu/get", () => { @@ -261,16 +243,7 @@ describe("getSFUConfigWithOpenID", () => { }; }); try { - await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - { - delayEndpointBaseUrl: "https://matrix.homeserverserver.org", - delayId: "mock_delay_id", - }, - ); + await getSFUConfig({ ...params, ...delayParams }); } catch (ex) { expect(ex).toBeInstanceOf(FailToGetOpenIdToken); expect((ex as FailToGetOpenIdToken).cause).toEqual( @@ -309,18 +282,13 @@ describe("getSFUConfigWithOpenID", () => { fetchMock.post("https://sfu.example.org/sfu/get", () => { return { status: 200, - body: { url: sfuUrl, jwt: testJWTToken }, + body: { url: serviceUrl, jwt: testJWTToken }, }; }); - const config = await getSFUConfigWithOpenID( - matrixClient, - ownMemberMock, - "https://sfu.example.org", - "!example_room_id", - ); + const config = await getSFUConfig({ ...params, ...delayParams }); expect(config).toEqual({ jwt: testJWTToken, - url: sfuUrl, + url: serviceUrl, livekitIdentity: "@me:example.org:ABCDEF", livekitAlias: "!example_room_id", }); diff --git a/src/livekit/auth/openID.ts b/src/livekit/auth/openID.ts index 93fdf31d7..1a0472094 100644 --- a/src/livekit/auth/openID.ts +++ b/src/livekit/auth/openID.ts @@ -24,22 +24,40 @@ export type ClientOpenIDParts = Pick< "getOpenIdToken" | "getDeviceId" >; +export interface GetSFUConfigParams { + client: ClientOpenIDParts; + /** + * Our own membership identity parts used to send to the JWT service. + */ + membership: CallMembershipIdentityParts; + /** + * The base URL of the LiveKit JWT service. + */ + serviceUrl: string; + /** + * The room ID used in the request. This is NOT the livekit_alias. The JWT + * service will provide the alias. It maps Matrix room IDs to LiveKit aliases. + */ + roomId: string; + /** + * Whether we want to publish or only subscribe on this transport. + */ + role: "publisher" | "subscriber"; + /** + * The base URL of the Matrix homeserver, for delayed leave delegation. + */ + delayEndpointBaseUrl?: string; + /** + * The delay ID of the leave event to be delegated to the JWT service. + */ + delayId?: string; + logger: Logger; +} + /** * Gets a bearer token from the homeserver and then use it to authenticate * to the matrix RTC backend in order to get acces to the SFU. * It has built-in retry for calls to the homeserver with a backoff policy. - * @param client The Matrix client - * @param membership Our own membership identity parts used to send to jwt service. - * @param serviceUrl The URL of the livekit SFU service - * @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases. - * @param opts Additional options to modify which endpoint with which data will be used to acquire the jwt token. - * @param opts.matrixRTCMode Determines which version of the JWT endpoint to use, which affects whether the - * RTC backend identity is based on string concatenation (legacy) or a hash (Matrix 2.0). - * This function by default uses whatever is possible with the current jwt service installed next to the SFU. - * For remote connections this does not matter, since we will not publish there we can rely on the newest option. - * @param opts.delayEndpointBaseUrl The URL of the matrix homeserver. - * @param opts.delayId The delay id used for the jwt service to manage. - * @param logger optional logger. * @returns Object containing the token information * @throws FailToGetOpenIdToken */ @@ -52,16 +70,7 @@ export async function getSFUConfig({ delayEndpointBaseUrl, delayId, logger, -}: { - client: ClientOpenIDParts; - membership: CallMembershipIdentityParts; - serviceUrl: string; - roomId: string; - role: "publisher" | "subscriber"; - delayEndpointBaseUrl?: string; - delayId?: string; - logger: Logger; -}): Promise { +}: GetSFUConfigParams): Promise { let openIdToken: IOpenIDToken; try { openIdToken = await doNetworkOperationWithRetry(async () => diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b8d7ab89d..b8c02d954 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -694,15 +694,19 @@ export const InCallView: FC = ({ tab={settingsTab} onTabChange={setSettingsTab} developerSettingsVm={developerSettingsVm} - livekitRooms={allConnections - .getConnections() - .map((connectionItem) => ({ - room: connectionItem.livekitRoom, - livekitAlias: connectionItem.livekitAlias, - // TODO compute is local or tag it in the livekit room items already - isLocal: undefined, - url: connectionItem.transport.livekit_service_url, - }))} + livekitRooms={allConnections.map( + ({ livekitRoom: room, livekitAlias, transport, role }) => ({ + room, + livekitAlias, + role, + url: + // It should be relatively clear to the user which kind of URL + // this is, based on the scheme (ws:// vs. http://) + "url" in transport + ? transport.url + : transport.livekit_service_url, + }), + )} /> )} diff --git a/src/settings/DeveloperSettingsTab.test.tsx b/src/settings/DeveloperSettingsTab.test.tsx index bd2b40b7d..53c1f507f 100644 --- a/src/settings/DeveloperSettingsTab.test.tsx +++ b/src/settings/DeveloperSettingsTab.test.tsx @@ -18,14 +18,14 @@ import { } from "./DeveloperSettingsTab"; import { outOfCallDeveloperSettingsTabViewModel } from "./DeveloperSettingsTabViewModel"; import { createStaticViewModel } from "../state/ViewModel"; -import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; import { - customLivekitUrl as customLivekitUrlSetting, + customTransport as customTransportSetting, enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting, matrixRTCMode as matrixRTCModeSetting, } from "./settings"; import { MatrixRTCMode } from "../config/ConfigOptions"; import { mockConfig } from "../utils/test"; +import { getSFUConfig } from "../livekit/auth"; // Mock url params hook to avoid environment-dependent snapshot churn. vi.mock("../UrlParams", () => ({ @@ -36,8 +36,8 @@ vi.mock("../UrlParams", () => ({ })); // IMPORTANT: mock the same specifier used by DeveloperSettingsTab -vi.mock("../livekit/openIDSFU", () => ({ - getSFUConfigWithOpenID: vi.fn().mockResolvedValue({ +vi.mock("../livekit/auth", () => ({ + getSFUConfig: vi.fn().mockResolvedValue({ url: "mock-url", jwt: "mock-jwt", }), @@ -126,7 +126,7 @@ describe("DeveloperSettingsTab", () => { }); describe("custom livekit url", () => { afterEach(() => { - customLivekitUrlSetting.setValue(null); + customTransportSetting.setValue(null); }); const client = { doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(true), @@ -153,9 +153,9 @@ describe("DeveloperSettingsTab", () => { const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); - expect(getSFUConfigWithOpenID).not.toHaveBeenCalled(); + expect(getSFUConfig).not.toHaveBeenCalled(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); it("will not update custom livekit url without text in input", async () => { const user = userEvent.setup(); @@ -176,9 +176,9 @@ describe("DeveloperSettingsTab", () => { const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); - expect(getSFUConfigWithOpenID).not.toHaveBeenCalled(); + expect(getSFUConfig).not.toHaveBeenCalled(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); it("will not update custom livekit url when pressing cancel", async () => { const user = userEvent.setup(); @@ -202,9 +202,9 @@ describe("DeveloperSettingsTab", () => { name: "Reset overwrite", }); await user.click(cancelButton); - expect(getSFUConfigWithOpenID).not.toHaveBeenCalled(); + expect(getSFUConfig).not.toHaveBeenCalled(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); it("will update custom livekit url", async () => { const user = userEvent.setup(); @@ -226,14 +226,14 @@ describe("DeveloperSettingsTab", () => { const saveButton = screen.getByRole("button", { name: "Save" }); await user.click(saveButton); - expect(getSFUConfigWithOpenID).toHaveBeenCalledWith( + expect(getSFUConfig).toHaveBeenCalledWith( expect.anything(), expect.anything(), "wss://example.livekit.valid", "#testRoom", ); - expect(customLivekitUrlSetting.getValue()).toBe( + expect(customTransportSetting.getValue()).toBe( "wss://example.livekit.valid", ); }); @@ -256,14 +256,14 @@ describe("DeveloperSettingsTab", () => { await user.type(input, "wss://example.livekit.valid"); const saveButton = screen.getByRole("button", { name: "Save" }); - (getSFUConfigWithOpenID as Mock).mockImplementation(() => { + (getSFUConfig as Mock).mockImplementation(() => { throw new Error("Invalid URL"); }); await user.click(saveButton); expect( screen.getByText("invalid URL (did not update)"), ).toBeInTheDocument(); - expect(customLivekitUrlSetting.getValue()).toBe(null); + expect(customTransportSetting.getValue()).toBe(null); }); }); diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index ec5bf7b08..eb5b1c898 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -45,7 +45,7 @@ import { muteAllAudio as muteAllAudioSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, matrixRTCMode as matrixRTCModeSetting, - customLivekitUrl as customLivekitUrlSetting, + customTransport as customTransportSetting, advancedScreenShare as advancedScreenShareSetting, screenShareResolution as screenShareResolutionSetting, screenShareFramerate as screenShareFramerateSetting, @@ -67,9 +67,9 @@ import styles from "./DeveloperSettingsTab.module.css"; import settingsStyles from "./SettingsModal.module.css"; import { Slider } from "../Slider"; import { useUrlParams } from "../UrlParams"; -import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; import { useBehavior } from "../useBehavior"; import { type ViewModel } from "../state/ViewModel.ts"; +import { getSFUConfig } from "../livekit/auth/index.ts"; /** * The state of MatrixRTC's media key rotation. @@ -167,16 +167,18 @@ export const DeveloperSettingsTab: FC = ({ enableExtendedLivekitLogsSetting, ); - const [customLivekitUrlUpdateError, setCustomLivekitUrlUpdateError] = - useState(null); - const [customLivekitUrl, setCustomLivekitUrl] = useSetting( - customLivekitUrlSetting, + const [customTransportUpdateError, setCustomLivekitUrlUpdateError] = useState< + string | null + >(null); + const [customTransport, setCustomTransport] = useSetting( + customTransportSetting, + ); + const [customTransportTextBuffer, setCustomLivekitUrlTextBuffer] = useState( + () => JSON.stringify(customTransport), ); - const [customLivekitUrlTextBuffer, setCustomLivekitUrlTextBuffer] = - useState(customLivekitUrl); useEffect(() => { - setCustomLivekitUrlTextBuffer(customLivekitUrl); - }, [customLivekitUrl]); + setCustomLivekitUrlTextBuffer(JSON.stringify(customTransport)); + }, [customTransport]); const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting); @@ -491,23 +493,23 @@ export const DeveloperSettingsTab: FC = ({ e.preventDefault()} helpLabel={ - customLivekitUrl === null - ? t("developer_mode.custom_livekit_url.from_config") - : t("developer_mode.custom_livekit_url.current_url") + - customLivekitUrl + customTransport === null + ? t("developer_mode.custom_transport.from_config") + : t("developer_mode.custom_transport.current") + + JSON.stringify(customTransport) } - label={t("developer_mode.custom_livekit_url.label")} - saveButtonLabel={t("developer_mode.custom_livekit_url.save")} - savingLabel={t("developer_mode.custom_livekit_url.saving")} - cancelButtonLabel={t("developer_mode.custom_livekit_url.reset")} + label={t("developer_mode.custom_transport.label")} + saveButtonLabel={t("developer_mode.custom_transport.save")} + savingLabel={t("developer_mode.custom_transport.saving")} + cancelButtonLabel={t("developer_mode.custom_transport.reset")} onSave={useCallback( async (e: React.FormEvent): Promise => { if ( roomId === undefined || - customLivekitUrlTextBuffer === "" || - customLivekitUrlTextBuffer === null + customTransportTextBuffer === "" || + customTransportTextBuffer === null ) { - setCustomLivekitUrl(null); + setCustomTransport(null); return; } @@ -518,21 +520,27 @@ export const DeveloperSettingsTab: FC = ({ if (userId === null || deviceId === null) { throw new Error("Invalid user or device ID"); } - await getSFUConfigWithOpenID( + const transport = JSON.parse(customTransportTextBuffer); + // Validate the transport by attempting to get a token for it + await getSFUConfig({ client, - { userId, deviceId, memberId: "" }, - customLivekitUrlTextBuffer, + membership: { userId, deviceId, memberId: "" }, + transport, roomId, - ); + role: "publisher", + logger, + }); setCustomLivekitUrlUpdateError(null); - setCustomLivekitUrl(customLivekitUrlTextBuffer); + setCustomTransport(transport); } catch { - setCustomLivekitUrlUpdateError("invalid URL (did not update)"); + setCustomLivekitUrlUpdateError( + "invalid transport (did not update)", + ); } }, - [customLivekitUrlTextBuffer, setCustomLivekitUrl, client, roomId], + [customTransportTextBuffer, setCustomTransport, client, roomId], )} - value={customLivekitUrlTextBuffer ?? ""} + value={customTransportTextBuffer ?? ""} onChange={useCallback( (event: ChangeEvent): void => { setCustomLivekitUrlTextBuffer(event.target.value); @@ -541,14 +549,14 @@ export const DeveloperSettingsTab: FC = ({ )} onCancel={useCallback( (e: React.FormEvent) => { - setCustomLivekitUrl(null); + setCustomTransport(null); }, - [setCustomLivekitUrl], + [setCustomTransport], )} - serverInvalid={customLivekitUrlUpdateError !== null} + serverInvalid={customTransportUpdateError !== null} > - {customLivekitUrlUpdateError !== null && ( - {customLivekitUrlUpdateError} + {customTransportUpdateError !== null && ( + {customTransportUpdateError} )} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 933ac66f5..733068acc 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -59,7 +59,7 @@ interface Props { livekitRooms?: { room: LivekitRoom; url: string; - isLocal?: boolean; + role: "publisher" | "subscriber"; }[]; /** Only available while in a call. Used by the developer tab. */ developerSettingsVm?: ViewModel; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index f108eb5f9..6b4029044 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -12,6 +12,7 @@ import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { type Behavior } from "../state/Behavior"; import { useBehavior } from "../useBehavior"; import { MatrixRTCMode } from "../config/ConfigOptions"; +import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; export class Setting { public constructor( @@ -153,8 +154,8 @@ export const matrixRTCMode = new Setting( MatrixRTCMode.Compatibility, ); -export const customLivekitUrl = new Setting( - "custom-livekit-url", +export const customTransport = new Setting( + "custom-transport", null, ); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 9084beb7d..06afaf9f2 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -39,6 +39,7 @@ import { timer, takeUntil, from, + concatWith, } from "rxjs"; import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { @@ -383,7 +384,7 @@ export interface CallViewModel { * key is generated when someone joins or leaves. */ keyRotationSuppressed$: Behavior; - allConnections$: Behavior; + allConnections$: Behavior; /** Participants sorted by livekit room so they can be used in the audio rendering */ livekitRoomItems$: Behavior; /** use the layout instead, this is just for the sdk export. */ @@ -591,6 +592,7 @@ export function createCallViewModel$( const connectionManager: IConnectionManager = { connectionManagerData$: scope.behavior( localTransport$.pipe( + concatWith(NEVER), // So the Observable doesn't complete prematurely mapScoped("connectionManager$", (scope, localTransport) => createConnectionManager$({ scope, @@ -738,13 +740,16 @@ export function createCallViewModel$( }); const allConnections$ = scope.behavior( - connectionManager.connectionManagerData$.pipe(map((d) => d.value)), + connectionManager.connectionManagerData$.pipe( + map((d) => d.value.getConnections()), + ), ); const livekitRoomItems$ = scope.behavior( allConnections$.pipe( - switchMap((connections) => - combineLatest( - connections.getConnections().map((connection) => + switchMap((connections) => { + if (connections.length === 0) return of([]); + return combineLatest( + connections.map((connection) => remoteMatrixLivekitMembers$.pipe( switchMap((members) => combineLatest(members.value.map((m) => m.participant.value$)), @@ -758,8 +763,8 @@ export function createCallViewModel$( })), ), ), - ), - ), + ); + }), ), ); diff --git a/src/state/CallViewModel/CallViewModelTestUtils.ts b/src/state/CallViewModel/CallViewModelTestUtils.ts index 0863ea2b7..a226bca19 100644 --- a/src/state/CallViewModel/CallViewModelTestUtils.ts +++ b/src/state/CallViewModel/CallViewModelTestUtils.ts @@ -30,7 +30,6 @@ import { type CallViewModelOptions, } from "./CallViewModel"; import { - exampleSfuConfig, exampleTransport, mockConfig, MockConnection, @@ -209,24 +208,21 @@ export function withCallViewModel(mode: MatrixRTCMode) { livekitRoomFactory, connectionState$, windowSize$, - localTransport: { - transport: exampleTransport, - sfuConfig: exampleSfuConfig, - }, + localTransport: exampleTransport, connectionFactory: { createConnection( scope, + role, transport, ownMembershipIdentity, logger, - sfuConfig, ) { return new MockConnection( { scope, + role, transport, ownMembershipIdentity, - existingSFUConfig: sfuConfig, client: room.client, roomId: room.roomId, livekitRoomFactory, diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index fe4632d39..244beeba2 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -58,8 +58,8 @@ import { ConnectionState, type Connection } from "../remoteMembers/Connection"; import { type Publisher } from "./Publisher"; import { initializeWidget } from "../../../widget"; import { nullHostBridge } from "../../../HostBridge"; -import { type LocalTransport } from "./LocalTransport"; -import * as openIDSFU from "../../../livekit/openIDSFU"; +import { type SFUConfig } from "../../../livekit/auth"; +import * as livekitAuth from "../../../livekit/auth"; initializeWidget(); @@ -110,6 +110,7 @@ const delegatedTimings: ResolvedDelayedLeaveTimings = { }; const mockedClient = { + baseUrl: "https://matrix.example.org", getDomain: vi.fn().mockReturnValue("example.org"), getDeviceId: vi.fn().mockReturnValue("AAAA"), getOpenIdToken: vi.fn().mockResolvedValue({ @@ -118,6 +119,7 @@ const mockedClient = { matrix_server_name: "localhost", expires_in: 10000, }), + _unstable_delegateDelayedLeave: vi.fn().mockResolvedValue({}), }; describe("enterRTCSession", () => { @@ -315,19 +317,17 @@ describe("LocalMembership", () => { scope.end(); }); - const mockTransportConfig = { - livekit_service_url: "a", + const mockTransport: UnstableLivekitTransport = { + type: "livekit", + url: "https://sfu.example.org", } as UnstableLivekitTransport; - const mockTransport = { - transport: mockTransportConfig, - sfuConfig: { - jwt: "foo", - livekitAlias: "bar", - livekitIdentity: "baz", - url: "bro", - }, - } as LocalTransport; + const mockSFUConfig: SFUConfig = { + jwt: "foo", + livekitAlias: "bar", + livekitIdentity: "baz", + url: "bro", + }; const connectionTransportAConnected = { livekitRoom: mockLivekitRoom({ @@ -337,7 +337,7 @@ describe("LocalMembership", () => { } as unknown as LocalParticipant, }), state$: constant(ConnectionState.LivekitConnected), - transport: mockTransportConfig, + transport: mockTransport, } as Connection; const connectionTransportAConnecting = { ...connectionTransportAConnected, @@ -346,8 +346,8 @@ describe("LocalMembership", () => { } as unknown as Connection; const authCallSpy = vi - .spyOn(openIDSFU, "getSFUConfigWithOpenID") - .mockImplementation(() => mockedClient.getOpenIdToken()); + .spyOn(livekitAuth, "getSFUConfig") + .mockResolvedValue(mockSFUConfig); afterEach(() => authCallSpy.mockClear()); it.each([ @@ -386,7 +386,7 @@ describe("LocalMembership", () => { await flushPromises(); // Joins with timings appropriate for the level of delegation support expect(joinMatrixRTC).toHaveBeenCalledWith( - mockTransportConfig, + mockTransport, delayedLeaveTimings, ); @@ -480,7 +480,7 @@ describe("LocalMembership", () => { it("tracks livekit state correctly", async () => { const scope = new ObservableScope(); const connectionManagerData = new ConnectionManagerData(); - const localTransport$ = new Subject(); + const localTransport$ = new Subject(); const connectionManagerData$ = new BehaviorSubject( new Epoch(connectionManagerData), @@ -553,7 +553,7 @@ describe("LocalMembership", () => { }); ( - connectionManagerData2.getConnectionForTransport(mockTransportConfig)! + connectionManagerData2.getConnectionForTransport(mockTransport)! .state$ as BehaviorSubject ).next(ConnectionState.LivekitConnected); expect(localMembership.localMemberState$.value).toStrictEqual({ @@ -858,7 +858,7 @@ describe("LocalMembership", () => { const setScreenShareEnabled = vi.fn().mockRejectedValue(error); const connection = { state$: constant(ConnectionState.LivekitConnected), - transport: mockTransportConfig, + transport: mockTransport, livekitRoom: mockLivekitRoom({ localParticipant: mockLocalParticipant({ isScreenShareEnabled: false, diff --git a/src/state/CallViewModel/localMember/LocalTransport.test.ts b/src/state/CallViewModel/localMember/LocalTransport.test.ts index 90d0d15e5..f1e146c6d 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.test.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.test.ts @@ -22,13 +22,13 @@ import { MatrixRTCTransportMissingError, FailToGetOpenIdToken, } from "../../../utils/errors"; -import * as openIDSFU from "../../../livekit/openIDSFU"; -import { customLivekitUrl } from "../../../settings/settings"; +import * as livekitAuth from "../../../livekit/auth"; +import { customTransport } from "../../../settings/settings"; import { testJWTToken } from "../../../utils/test-fixtures"; -import { MatrixRTCMode } from "../../../config/ConfigOptions"; +import { type MatrixClient } from "matrix-js-sdk"; describe("LocalTransport", () => { - const openIdResponse: openIDSFU.SFUConfig = { + const sfuConfig: livekitAuth.SFUConfig = { url: "https://lk.example.org", jwt: testJWTToken, livekitAlias: "Akph4alDMhen", @@ -40,42 +40,25 @@ describe("LocalTransport", () => { it("throws if config is missing", async () => { await expect( getLocalTransport({ - roomId: "!room:example.org", - client: { - // eslint-disable-next-line @typescript-eslint/naming-convention - _unstable_getRTCTransports: async () => Promise.resolve([]), - getDomain: () => "example.org", - // These won't be called in this error path but satisfy the type - getOpenIdToken: vi.fn(), - getDeviceId: vi.fn(), - }, - ownMembershipIdentity: ownMemberMock, - matrixRTCMode: MatrixRTCMode.Compatibility, + _unstable_getRTCTransports: async () => Promise.resolve([]), + getDomain: () => "example.org", }), ).rejects.toThrow(new MatrixRTCTransportMissingError("example.org")); }); - it("throws FailToGetOpenIdToken when OpenID fetch fails", async () => { + it("threes if SFU config fetch fails", async () => { // Provide a valid config so makeTransportInternal resolves a transport mockConfig({ livekit: { livekit_service_url: "https://lk.example.org" }, }); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockRejectedValue( + vi.spyOn(livekitAuth, "getSFUConfig").mockRejectedValue( new FailToGetOpenIdToken(new Error("no openid")), ); await expect( getLocalTransport({ - roomId: "!example_room_id", - client: { - getDomain: () => "example.org", - // eslint-disable-next-line @typescript-eslint/naming-convention - _unstable_getRTCTransports: async () => Promise.resolve([]), - getOpenIdToken: vi.fn(), - getDeviceId: vi.fn(), - }, - ownMembershipIdentity: ownMemberMock, - matrixRTCMode: MatrixRTCMode.Compatibility, + getDomain: () => "example.org", + _unstable_getRTCTransports: async () => Promise.resolve([]), }), ).rejects.toThrow(new FailToGetOpenIdToken(new Error("no openid"))); }); @@ -86,7 +69,7 @@ describe("LocalTransport", () => { livekit: { livekit_service_url: "https://lk.example.org" }, }); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue({ + vi.spyOn(livekitAuth, "getSFUConfig").mockResolvedValue({ url: "https://lk.example.org", jwt: "jwt", livekitAlias: "Akph4alDMhen", @@ -95,16 +78,8 @@ describe("LocalTransport", () => { expect( await getLocalTransport({ - roomId: "!room:example.org", - client: { - // eslint-disable-next-line @typescript-eslint/naming-convention - _unstable_getRTCTransports: async () => Promise.resolve([]), - getDomain: () => "example.org", - getOpenIdToken: vi.fn(), - getDeviceId: vi.fn(), - }, - ownMembershipIdentity: ownMemberMock, - matrixRTCMode: MatrixRTCMode.Compatibility, + _unstable_getRTCTransports: async () => Promise.resolve([]), + getDomain: () => "example.org", }), ).toStrictEqual({ transport: { @@ -120,26 +95,16 @@ describe("LocalTransport", () => { }); }); - type LocalTransportProps = Parameters[0]; - describe("transport configuration mechanisms", () => { - let localTransportOpts: LocalTransportProps & { - client: MockedObject; - }; + let client: MockedObject< + Pick + >; beforeEach(() => { mockConfig({}); - customLivekitUrl.setValue(customLivekitUrl.defaultValue); - localTransportOpts = { - ownMembershipIdentity: ownMemberMock, - roomId: "!example_room_id", - matrixRTCMode: MatrixRTCMode.Compatibility, - client: { - getDomain: vi.fn().mockReturnValue("example.org"), - // eslint-disable-next-line @typescript-eslint/naming-convention - _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), - getOpenIdToken: vi.fn(), - getDeviceId: vi.fn(), - }, + customTransport.setValue(customTransport.defaultValue); + client = { + getDomain: vi.fn().mockReturnValue("example.org"), + _unstable_getRTCTransports: vi.fn().mockResolvedValue([]), }; }); @@ -151,11 +116,9 @@ describe("LocalTransport", () => { mockConfig({ livekit: { livekit_service_url: "https://lk.example.org" }, }); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( - openIdResponse, - ); + vi.spyOn(livekitAuth, "getSFUConfig").mockResolvedValue(sfuConfig); - expect(await getLocalTransport(localTransportOpts)).toStrictEqual({ + expect(await getLocalTransport(client)).toStrictEqual({ transport: { livekit_service_url: "https://lk.example.org", type: "livekit", @@ -170,12 +133,13 @@ describe("LocalTransport", () => { }); it("supports getting transport via user settings", async () => { - customLivekitUrl.setValue("https://lk.example.org"); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( - openIdResponse, - ); + customTransport.setValue({ + type: "livekit", + livekit_service_url: "https://lk.example.org", + }); + vi.spyOn(livekitAuth, "getSFUConfig").mockResolvedValue(sfuConfig); - expect(await getLocalTransport(localTransportOpts)).toStrictEqual({ + expect(await getLocalTransport(client)).toStrictEqual({ transport: { livekit_service_url: "https://lk.example.org", type: "livekit", @@ -190,14 +154,12 @@ describe("LocalTransport", () => { }); it("supports getting transport via backend", async () => { - localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([ + client._unstable_getRTCTransports.mockResolvedValue([ { type: "livekit", livekit_service_url: "https://lk.example.org" }, ]); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockResolvedValue( - openIdResponse, - ); + vi.spyOn(livekitAuth, "getSFUConfig").mockResolvedValue(sfuConfig); - expect(await getLocalTransport(localTransportOpts)).toStrictEqual({ + expect(await getLocalTransport(client)).toStrictEqual({ transport: { livekit_service_url: "https://lk.example.org", type: "livekit", @@ -212,14 +174,14 @@ describe("LocalTransport", () => { }); it("fails fast if the openID request fails for backend config", async () => { - localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([ + client._unstable_getRTCTransports.mockResolvedValue([ { type: "livekit", livekit_service_url: "https://lk.example.org" }, ]); - vi.spyOn(openIDSFU, "getSFUConfigWithOpenID").mockRejectedValue( + vi.spyOn(livekitAuth, "getSFUConfig").mockRejectedValue( new FailToGetOpenIdToken(new Error("Test driven error")), ); - await expect(getLocalTransport(localTransportOpts)).rejects.toThrow( + await expect(getLocalTransport(client)).rejects.toThrow( expect.any(FailToGetOpenIdToken), ); }); @@ -227,17 +189,8 @@ describe("LocalTransport", () => { it("throws if no options are available", async () => { await expect( getLocalTransport({ - ownMembershipIdentity: ownMemberMock, - roomId: "!example_room_id", - matrixRTCMode: MatrixRTCMode.Compatibility, - client: { - getDomain: () => "example.org", - // eslint-disable-next-line @typescript-eslint/naming-convention - _unstable_getRTCTransports: async () => Promise.resolve([]), - // These won't be called in this error path but satisfy the type - getOpenIdToken: vi.fn(), - getDeviceId: vi.fn(), - }, + getDomain: () => "example.org", + _unstable_getRTCTransports: async () => Promise.resolve([]), }), ).rejects.toThrow(new MatrixRTCTransportMissingError("example.org")); }); diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 028ab1703..adfc07efb 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -11,7 +11,7 @@ import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { Config } from "../../../config/Config.ts"; import { MatrixRTCTransportMissingError } from "../../../utils/errors.ts"; -import { customLivekitUrl } from "../../../settings/settings.ts"; +import { customTransport as customTransportSetting } from "../../../settings/settings.ts"; import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; /** @@ -22,16 +22,15 @@ export async function getLocalTransport( client: Pick, ): Promise { const discovery = new RtcTransportAutoDiscovery({ - client: client, + client, resolvedConfig: Config.get(), logger: rootLogger.getChild("[LocalTransport]"), }); - const customUrl = customLivekitUrl.value$.value; - // Respect the user's custom URL, if set - const transport: UnstableLivekitTransport | null = customUrl - ? { type: "livekit", livekit_service_url: customUrl } - : await discovery.discoverPreferredTransport(); + // Respect the user's custom transport, if set + const transport = + customTransportSetting.value$.value ?? + (await discovery.discoverPreferredTransport()); if (transport === null) throw new MatrixRTCTransportMissingError(client.getDomain() ?? ""); diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 139786567..571fc6298 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -28,7 +28,7 @@ import { } from "livekit-client"; import fetchMock from "fetch-mock"; import EventEmitter from "events"; -import { type IOpenIDToken } from "matrix-js-sdk"; +import { MatrixError, type IOpenIDToken } from "matrix-js-sdk"; import { logger, type Logger } from "matrix-js-sdk/lib/logger"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; @@ -38,17 +38,20 @@ import { type ConnectionOpts, } from "./Connection.ts"; import { ObservableScope } from "../../ObservableScope.ts"; -import { type ClientOpenIDParts } from "../../../livekit/openIDSFU.ts"; import { ElementCallError, FailToGetOpenIdToken, } from "../../../utils/errors.ts"; import { testJWTToken } from "../../../utils/test-fixtures.ts"; import { mockRemoteParticipant, ownMemberMock } from "../../../utils/test.ts"; +import { + type ClientGetTokenParts, + type ClientOpenIDParts, +} from "../../../livekit/auth"; let testScope: ObservableScope; -let client: MockedObject; +let client: MockedObject; let fakeLivekitRoom: MockedObject; @@ -57,14 +60,14 @@ let fakeLocalParticipant: MockedObject; const ROOM_ID = "!roomID:example.org"; -const livekitFocus: UnstableLivekitTransport = { - livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt", +const transport: UnstableLivekitTransport = { type: "livekit", + url: "wss://matrix-rtc.m.localhost/livekit/sfu", }; function setupTest(): void { testScope = new ObservableScope(); - client = vi.mocked({ + client = vi.mocked({ getOpenIdToken: vi.fn().mockResolvedValue({ access_token: "rYsmGUEwNjKgJYyeNUkZseJN", token_type: "Bearer", @@ -72,7 +75,8 @@ function setupTest(): void { expires_in: 3600, }), getDeviceId: vi.fn().mockReturnValue("ABCDEF"), - } as unknown as ClientOpenIDParts); + _unstable_getLivekitToken: vi.fn().mockResolvedValue({ jwt: testJWTToken }), + }); localParticipantEventEmiter = new EventEmitter(); @@ -118,22 +122,13 @@ function setupRemoteConnection(): Connection { const opts: ConnectionOpts = { client: client, roomId: ROOM_ID, - transport: livekitFocus, + role: "subscriber", + transport: transport, scope: testScope, ownMembershipIdentity: ownMemberMock, livekitRoomFactory: () => fakeLivekitRoom, }; - fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, () => { - return { - status: 200, - body: { - url: "wss://matrix-rtc.m.localhost/livekit/sfu", - jwt: testJWTToken, - }, - }; - }); - fakeLivekitRoom.connect.mockImplementation(async (): Promise => { const changeEv = RoomEvent.ConnectionStateChanged; @@ -174,7 +169,8 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, roomId: ROOM_ID, - transport: livekitFocus, + role: "subscriber", + transport: transport, scope: testScope, ownMembershipIdentity: ownMemberMock, livekitRoomFactory: () => fakeLivekitRoom, @@ -191,7 +187,8 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, roomId: ROOM_ID, - transport: livekitFocus, + role: "subscriber", + transport: transport, scope: testScope, ownMembershipIdentity: ownMemberMock, livekitRoomFactory: () => fakeLivekitRoom, @@ -228,7 +225,7 @@ describe("Start connection states", () => { capturedState = capturedStates.pop(); if (capturedState instanceof Error) { expect(capturedState.message).toEqual("Something went wrong"); - expect(connection.transport).toEqual(livekitFocus); + expect(connection.transport).toEqual(transport); } else { expect.fail("Expected FailedToStart state but got " + capturedState); } @@ -241,7 +238,8 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, roomId: ROOM_ID, - transport: livekitFocus, + role: "subscriber", + transport: transport, scope: testScope, ownMembershipIdentity: ownMemberMock, livekitRoomFactory: () => fakeLivekitRoom, @@ -256,16 +254,15 @@ describe("Start connection states", () => { onTestFinished(() => s.unsubscribe()); const deferredSFU = Promise.withResolvers(); - // mock the /sfu/get call - fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, async () => { + client._unstable_getLivekitToken.mockImplementation(async () => { await deferredSFU.promise; - return { - status: 500, - body: { + throw new MatrixError( + { errcode: "M_LOOKUP_FAILED", error: "Failed to look up user info from homeserver", }, - }; + 500, + ); }); connection.start().catch(() => { @@ -288,7 +285,7 @@ describe("Start connection states", () => { expect(capturedState.cause.message).toContain( "Failed to look up user info from homeserver", ); - expect(connection.transport).toEqual(livekitFocus); + expect(connection.transport).toEqual(transport); } else { expect.fail("Expected FailedToStart state but got " + capturedState); } @@ -301,7 +298,8 @@ describe("Start connection states", () => { const opts: ConnectionOpts = { client: client, roomId: ROOM_ID, - transport: livekitFocus, + role: "subscriber", + transport: transport, scope: testScope, ownMembershipIdentity: ownMemberMock, livekitRoomFactory: () => fakeLivekitRoom, @@ -316,17 +314,6 @@ describe("Start connection states", () => { onTestFinished(() => s.unsubscribe()); const deferredSFU = Promise.withResolvers(); - // mock the /sfu/get call - fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, () => { - return { - status: 200, - body: { - url: "wss://matrix-rtc.m.localhost/livekit/sfu", - jwt: testJWTToken, - }, - }; - }); - fakeLivekitRoom.connect.mockImplementation(async () => { await deferredSFU.promise; throw new Error("Failed to connect to livekit"); @@ -353,7 +340,7 @@ describe("Start connection states", () => { expect(capturedState.cause.message).toContain( "Failed to connect to livekit", ); - expect(connection.transport).toEqual(livekitFocus); + expect(connection.transport).toEqual(transport); } else { expect.fail( "Expected FailedToStart state but got " + JSON.stringify(capturedState), @@ -550,7 +537,8 @@ describe("remote track logging", () => { { client, roomId: ROOM_ID, - transport: livekitFocus, + role: "subscriber", + transport: transport, scope: testScope, ownMembershipIdentity: ownMemberMock, livekitRoomFactory: () => fakeLivekitRoom, diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index 4c58560e8..d142dee32 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -102,7 +102,10 @@ export class Connection { */ public readonly state$: Behavior = this._state$; - private readonly role: "publisher" | "subscriber"; + /** + * Whether we want to publish or only subscribe on this connection. + */ + public readonly role: "publisher" | "subscriber"; /** * The media transport to connect to. diff --git a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts index 6b8d7feb9..d0b4c82a1 100644 --- a/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts +++ b/src/state/CallViewModel/remoteMembers/ConnectionManager.test.ts @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { BehaviorSubject, NEVER } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type RemoteParticipant } from "livekit-client"; import { logger } from "matrix-js-sdk/lib/logger"; @@ -74,7 +74,7 @@ describe("connections$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - localTransport$: NEVER, + localTransport: TRANSPORT_1, remoteTransports$: behavior("a", { a: new Epoch([TRANSPORT_1, TRANSPORT_2], 0), }), @@ -115,7 +115,7 @@ describe("connections$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - localTransport$: NEVER, + localTransport: TRANSPORT_1, remoteTransports$: behavior("abcdef", { a: new Epoch([TRANSPORT_1], 0), b: new Epoch([TRANSPORT_1], 1), @@ -162,7 +162,7 @@ describe("connections$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - localTransport$: NEVER, + localTransport: TRANSPORT_1, remoteTransports$: behavior("abc", { a: new Epoch([TRANSPORT_1], 0), b: new Epoch([TRANSPORT_1, TRANSPORT_2], 1), @@ -226,7 +226,12 @@ describe("connectionManagerData$ stream", () => { let fakeRemoteParticipantsStreams: Map>; function keyForTransport(transport: UnstableLivekitTransport): string { - return `${transport.livekit_service_url}`; + return JSON.stringify([ + "url" in transport ? transport.url : undefined, + "livekit_service_url" in transport + ? transport.livekit_service_url + : undefined, + ]); } beforeEach(() => { @@ -297,7 +302,7 @@ describe("connectionManagerData$ stream", () => { const { connectionManagerData$ } = createConnectionManager$({ scope: testScope, connectionFactory: fakeConnectionFactory, - localTransport$: NEVER, + localTransport: TRANSPORT_1, remoteTransports$: behavior("a", { a: new Epoch([TRANSPORT_1, TRANSPORT_2], 0), }), diff --git a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts index c8f511348..3135150d6 100644 --- a/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts +++ b/src/state/CallViewModel/remoteMembers/ECConnectionFactory.test.ts @@ -14,7 +14,10 @@ import EventEmitter from "events"; import { ObservableScope } from "../../ObservableScope.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts"; -import type { ClientOpenIDParts } from "../../../livekit/openIDSFU.ts"; +import { + type ClientGetTokenParts, + type ClientOpenIDParts, +} from "../../../livekit/auth"; import { exampleTransport, mockMediaDevices, @@ -32,6 +35,7 @@ import { cameraBitrate, cameraCodec, } from "../../../settings/settings.ts"; +import { testJWTToken } from "../../../utils/test-fixtures.ts"; // At the top of your test file, after imports vi.mock("livekit-client", async (importOriginal) => { @@ -51,13 +55,14 @@ vi.mock("livekit-client", async (importOriginal) => { }); let testScope: ObservableScope; -let mockClient: ClientOpenIDParts; +let mockClient: ClientGetTokenParts & ClientOpenIDParts; beforeEach(() => { testScope = new ObservableScope(); mockClient = { getOpenIdToken: vi.fn().mockReturnValue(""), getDeviceId: vi.fn().mockReturnValue("DEV000"), + _unstable_getLivekitToken: vi.fn().mockResolvedValue({ jwt: testJWTToken }), }; }); @@ -89,6 +94,7 @@ describe("ECConnectionFactory - Audio inputs options", () => { ); ecConnectionFactory.createConnection( testScope, + "subscriber", exampleTransport, ownMemberMock, logger, @@ -137,6 +143,7 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => { ); ecConnectionFactory.createConnection( testScope, + "subscriber", exampleTransport, ownMemberMock, logger, @@ -172,6 +179,7 @@ describe("ECConnectionFactory - Camera quality settings", () => { ); ecConnectionFactory.createConnection( testScope, + "subscriber", exampleTransport, ownMemberMock, logger, @@ -209,6 +217,7 @@ describe("ECConnectionFactory - Camera quality settings", () => { ); ecConnectionFactory.createConnection( testScope, + "subscriber", exampleTransport, ownMemberMock, logger, @@ -247,6 +256,7 @@ describe("ECConnectionFactory - Camera quality settings", () => { ); ecConnectionFactory.createConnection( testScope, + "subscriber", exampleTransport, ownMemberMock, logger, diff --git a/src/state/CallViewModel/remoteMembers/integration.test.ts b/src/state/CallViewModel/remoteMembers/integration.test.ts index 760128a06..d8ad8a92e 100644 --- a/src/state/CallViewModel/remoteMembers/integration.test.ts +++ b/src/state/CallViewModel/remoteMembers/integration.test.ts @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { test, vi, expect, beforeEach, afterEach } from "vitest"; -import { BehaviorSubject, NEVER } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { type Room as LivekitRoom } from "livekit-client"; import EventEmitter from "events"; import fetchMock from "fetch-mock"; @@ -19,8 +19,12 @@ import { trackEpoch, } from "../../ObservableScope.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts"; -import { type ClientOpenIDParts } from "../../../livekit/openIDSFU.ts"; import { + type ClientGetTokenParts, + type ClientOpenIDParts, +} from "../../../livekit/auth"; +import { + exampleTransport, mockMediaDevices, mockRtcMembership, ownMemberMock, @@ -40,7 +44,7 @@ import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts"; let testScope: ObservableScope; let ecConnectionFactory: ECConnectionFactory; -let mockClient: ClientOpenIDParts; +let mockClient: ClientGetTokenParts & ClientOpenIDParts; let lkRoomFactory: () => LivekitRoom; const createdMockLivekitRooms: Map = new Map(); @@ -50,6 +54,7 @@ beforeEach(() => { mockClient = { getOpenIdToken: vi.fn().mockReturnValue(""), getDeviceId: vi.fn().mockReturnValue("DEV000"), + _unstable_getLivekitToken: vi.fn().mockResolvedValue({ jwt: testJWTToken }), }; lkRoomFactory = vi.fn().mockImplementation(() => { @@ -123,7 +128,7 @@ test("bob, carl, then bob joining no tracks yet", () => { const connectionManager = createConnectionManager$({ scope: testScope, connectionFactory: ecConnectionFactory, - localTransport$: NEVER, + localTransport: exampleTransport, remoteTransports$: membershipsAndTransports.transports$, logger: logger, ownMembershipIdentity: ownMemberMock, diff --git a/src/utils/test.ts b/src/utils/test.ts index 00e60c762..7fb5502ad 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -78,7 +78,7 @@ import { type RemoteScreenShareViewModel, } from "../state/media/RemoteScreenShareViewModel"; import { Connection } from "../state/CallViewModel/remoteMembers/Connection"; -import { type SFUConfig } from "../livekit/openIDSFU"; +import { type SFUConfig } from "../livekit/auth"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -214,13 +214,6 @@ export const exampleTransport: UnstableLivekitTransport = { livekit_service_url: "https://lk.example.org", }; -export const exampleSfuConfig: SFUConfig = { - jwt: "foo", - livekitAlias: "bar", - livekitIdentity: "baz", - url: "bro", -}; - export function mockRtcMembership( user: string | RoomMember, deviceId: string, @@ -261,11 +254,12 @@ export function mockRtcMembership( content: data, }); - const membershipData = CallMembership.membershipDataFromMatrixEvent(event); + const backendIdentity = rtcBackendIdentity ?? `${userId}:${deviceId}`; const cms = new CallMembership( event, - membershipData, - rtcBackendIdentity ?? `${userId}:${deviceId}`, + CallMembership.membershipDataFromMatrixEvent(event), + [backendIdentity], + backendIdentity, ); vi.mocked(cms).getTransport = vi.fn().mockReturnValue(fociPreferred[0]);