diff --git a/playwright/restricted-sfu.spec.ts b/playwright/restricted-sfu.spec.ts new file mode 100644 index 00000000..a9e07d38 --- /dev/null +++ b/playwright/restricted-sfu.spec.ts @@ -0,0 +1,75 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { expect, test } from "@playwright/test"; +import { sleep } from "matrix-js-sdk/lib/utils.js"; + +test("Should request JWT token before starting the call", async ({ page }) => { + await page.goto("/"); + + let sfGetTimestamp = 0; + let sendStateEventTimestamp = 0; + await page.route( + "**/matrix-rtc.m.localhost/livekit/jwt/sfu/get", + async (route) => { + await sleep(2000); // Simulate very slow request + await route.continue(); + sfGetTimestamp = Date.now(); + }, + ); + + await page.route( + "**/state/org.matrix.msc3401.call.member/**", + async (route) => { + await route.continue(); + sendStateEventTimestamp = Date.now(); + }, + ); + + await page.getByTestId("home_callName").click(); + await page.getByTestId("home_callName").fill("HelloCall"); + await page.getByTestId("home_displayName").click(); + await page.getByTestId("home_displayName").fill("John Doe"); + await page.getByTestId("home_go").click(); + + // Join the call + await page.getByTestId("lobby_joinCall").click(); + await page.waitForTimeout(4000); + // Ensure that the call is connected + await page + .locator("div") + .filter({ hasText: /^HelloCall$/ }) + .click(); + + expect(sfGetTimestamp).toBeGreaterThan(0); + expect(sendStateEventTimestamp).toBeGreaterThan(0); + expect(sfGetTimestamp).toBeLessThan(sendStateEventTimestamp); +}); + +test("Error when pre-warming the focus are caught by the ErrorBoundary", async ({ + page, +}) => { + await page.goto("/"); + + await page.route("**/openid/request_token", async (route) => { + await route.fulfill({ + status: 418, // Simulate an error not retryable + }); + }); + + await page.getByTestId("home_callName").click(); + await page.getByTestId("home_callName").fill("HelloCall"); + await page.getByTestId("home_displayName").click(); + await page.getByTestId("home_displayName").fill("John Doe"); + await page.getByTestId("home_go").click(); + + // Join the call + await page.getByTestId("lobby_joinCall").click(); + + // Should fail + await expect(page.getByText("Something went wrong")).toBeVisible(); +}); diff --git a/playwright/sfu-reconnect-bug.spec.ts b/playwright/sfu-reconnect-bug.spec.ts index c756570a..6138eb78 100644 --- a/playwright/sfu-reconnect-bug.spec.ts +++ b/playwright/sfu-reconnect-bug.spec.ts @@ -100,5 +100,5 @@ test("When creator left, avoid reconnect to the same SFU", async ({ // https://github.com/element-hq/element-call/issues/3344 // The app used to request a new jwt token then to reconnect to the SFU expect(wsConnectionCount).toBe(1); - expect(sfuGetCallCount).toBe(1); + expect(sfuGetCallCount).toBe(2 /* the first one is for the warmup */); }); diff --git a/src/rtcSessionHelpers.test.ts b/src/rtcSessionHelpers.test.ts index ecfd44f7..2ef9e3f1 100644 --- a/src/rtcSessionHelpers.test.ts +++ b/src/rtcSessionHelpers.test.ts @@ -70,6 +70,12 @@ test("It joins the correct Session", async () => { roomId: "roomId", client: { getDomain: vi.fn().mockReturnValue("example.org"), + getOpenIdToken: vi.fn().mockResolvedValue({ + access_token: "ACCCESS_TOKEN", + token_type: "Bearer", + matrix_server_name: "localhost", + expires_in: 10000, + }), }, }, memberships: [], @@ -195,6 +201,12 @@ test("It should not fail with configuration error if homeserver config has livek roomId: "roomId", client: { getDomain: vi.fn().mockReturnValue("example.org"), + getOpenIdToken: vi.fn().mockResolvedValue({ + access_token: "ACCCESS_TOKEN", + token_type: "Bearer", + matrix_server_name: "localhost", + expires_in: 10000, + }), }, }, memberships: [], diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index be5eedf1..73f58cea 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; -import { logger } from "matrix-js-sdk/lib/logger"; import { isLivekitFocus, isLivekitFocusConfig, type LivekitFocus, type LivekitFocusActive, + type MatrixRTCSession, } from "matrix-js-sdk/lib/matrixrtc"; +import { logger } from "matrix-js-sdk/lib/logger"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { PosthogAnalytics } from "./analytics/PosthogAnalytics"; @@ -20,6 +20,7 @@ import { Config } from "./config/Config"; import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget"; import { MatrixRTCFocusMissingError } from "./utils/errors"; import { getUrlParams } from "./UrlParams"; +import { getSFUConfigWithOpenID } from "./livekit/openIDSFU.ts"; const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci"; @@ -46,6 +47,9 @@ async function makePreferredLivekitFoci( preferredFoci.push(focusInUse); } + // Warm up the first focus we owned, to ensure livekit room is created before any state event sent. + let toWarmUp: LivekitFocus | undefined; + // Prioritize the .well-known/matrix/client, if available, over the configured SFU const domain = rtcSession.room.client.getDomain(); if (domain) { @@ -55,18 +59,17 @@ async function makePreferredLivekitFoci( FOCI_WK_KEY ]; if (Array.isArray(wellKnownFoci)) { - preferredFoci.push( - ...wellKnownFoci - .filter((f) => !!f) - .filter(isLivekitFocusConfig) - .map((wellKnownFocus) => { - logger.log( - "Adding livekit focus from well known: ", - wellKnownFocus, - ); - return { ...wellKnownFocus, livekit_alias: livekitAlias }; - }), - ); + const validWellKnownFoci = wellKnownFoci + .filter((f) => !!f) + .filter(isLivekitFocusConfig) + .map((wellKnownFocus) => { + logger.log("Adding livekit focus from well known: ", wellKnownFocus); + return { ...wellKnownFocus, livekit_alias: livekitAlias }; + }); + if (validWellKnownFoci.length > 0) { + toWarmUp = validWellKnownFoci[0]; + } + preferredFoci.push(...validWellKnownFoci); } } @@ -77,10 +80,15 @@ async function makePreferredLivekitFoci( livekit_service_url: urlFromConf, livekit_alias: livekitAlias, }; + toWarmUp = toWarmUp ?? focusFormConf; logger.log("Adding livekit focus from config: ", focusFormConf); preferredFoci.push(focusFormConf); } + if (toWarmUp) { + // this will call the jwt/sfu/get endpoint to pre create the livekit room. + await getSFUConfigWithOpenID(rtcSession.room.client, toWarmUp); + } if (preferredFoci.length === 0) throw new MatrixRTCFocusMissingError(domain ?? ""); return Promise.resolve(preferredFoci);