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