mirror of
https://github.com/vector-im/element-call.git
synced 2026-01-18 02:32:27 +00:00
Error Handling: gracefully handle missing livekit service url in config
This commit is contained in:
@@ -85,7 +85,8 @@
|
||||
"insufficient_capacity": "Insufficient capacity",
|
||||
"insufficient_capacity_description": "The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.",
|
||||
"open_elsewhere": "Opened in another tab",
|
||||
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page."
|
||||
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
|
||||
"unexpected_ec_error": "An unexpected error occurred (<0>Error Code:</0> <1>{{ errorCode }}</1>). Please contact your server administrator."
|
||||
},
|
||||
"group_call_loader": {
|
||||
"banned_body": "You have been banned from the room.",
|
||||
|
||||
@@ -6,13 +6,14 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import {
|
||||
HostIcon,
|
||||
PopOutIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { ErrorView } from "./ErrorView";
|
||||
import { type ElementCallError, type ErrorCode } from "./utils/ec-errors.ts";
|
||||
|
||||
/**
|
||||
* An error consisting of a terse message to be logged to the console and a
|
||||
@@ -65,3 +66,33 @@ export class InsufficientCapacityError extends RichError {
|
||||
super("Insufficient server capacity", <InsufficientCapacity />);
|
||||
}
|
||||
}
|
||||
|
||||
type ECErrorProps = {
|
||||
errorCode: ErrorCode;
|
||||
};
|
||||
|
||||
const GenericECError: FC<{ errorCode: ErrorCode }> = ({
|
||||
errorCode,
|
||||
}: ECErrorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ErrorView Icon={HostIcon} title={t("error.generic")}>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="error.unexpected_ec_error"
|
||||
components={[<b />, <code />]}
|
||||
values={{ errorCode }}
|
||||
/>
|
||||
</p>
|
||||
</ErrorView>
|
||||
);
|
||||
};
|
||||
|
||||
export class ElementCallRichError extends RichError {
|
||||
public ecError: ElementCallError;
|
||||
public constructor(ecError: ElementCallError) {
|
||||
super(ecError.message, <GenericECError errorCode={ecError.code} />);
|
||||
this.ecError = ecError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import {
|
||||
Room,
|
||||
isE2EESupported as isE2EESupportedBrowser,
|
||||
Room,
|
||||
} from "livekit-client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
@@ -62,6 +62,7 @@ import { useLatest } from "../useLatest";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { ErrorView } from "../ErrorView";
|
||||
import { ConnectionLostError, ElementCallError } from "../utils/ec-errors.ts";
|
||||
import { ElementCallRichError } from "../RichError.tsx";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -166,6 +167,22 @@ export const GroupCallView: FC<Props> = ({
|
||||
const latestDevices = useLatest(deviceContext);
|
||||
const latestMuteStates = useLatest(muteStates);
|
||||
|
||||
const enterRTCSessionOrError = async (
|
||||
rtcSession: MatrixRTCSession,
|
||||
perParticipantE2EE: boolean,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
} catch (e) {
|
||||
if (e instanceof ElementCallError) {
|
||||
// e.code === ErrorCode.MISSING_LIVE_KIT_SERVICE_URL)
|
||||
setEnterRTCError(e);
|
||||
} else {
|
||||
logger.error(`Unknown Error while entering RTC session`, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const defaultDeviceSetup = async ({
|
||||
audioInput,
|
||||
@@ -215,7 +232,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
await defaultDeviceSetup(
|
||||
ev.detail.data as unknown as JoinCallData,
|
||||
);
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
|
||||
widget.api.transport.reply(ev.detail, {});
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
@@ -228,13 +245,13 @@ export const GroupCallView: FC<Props> = ({
|
||||
} else {
|
||||
// No lobby and no preload: we enter the rtc session right away
|
||||
(async (): Promise<void> => {
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
await enterRTCSessionOrError(rtcSession, perParticipantE2EE);
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session", e);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
void enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
void enterRTCSessionOrError(rtcSession, perParticipantE2EE);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
@@ -248,6 +265,9 @@ export const GroupCallView: FC<Props> = ({
|
||||
]);
|
||||
|
||||
const [left, setLeft] = useState(false);
|
||||
const [enterRTCError, setEnterRTCError] = useState<ElementCallError | null>(
|
||||
null,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onLeave = useCallback(
|
||||
@@ -348,8 +368,8 @@ export const GroupCallView: FC<Props> = ({
|
||||
const onReconnect = useCallback(() => {
|
||||
setLeft(false);
|
||||
resetError();
|
||||
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
|
||||
logger.error("Error re-entering RTC session on reconnect", e);
|
||||
enterRTCSessionOrError(rtcSession, perParticipantE2EE).catch((e) => {
|
||||
logger.error("Error re-entering RTC session", e);
|
||||
});
|
||||
}, [resetError]);
|
||||
|
||||
@@ -398,7 +418,9 @@ export const GroupCallView: FC<Props> = ({
|
||||
client={client}
|
||||
matrixInfo={matrixInfo}
|
||||
muteStates={muteStates}
|
||||
onEnter={() => void enterRTCSession(rtcSession, perParticipantE2EE)}
|
||||
onEnter={() =>
|
||||
void enterRTCSessionOrError(rtcSession, perParticipantE2EE)
|
||||
}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={hideHeader}
|
||||
participantCount={participantCount}
|
||||
@@ -408,7 +430,14 @@ export const GroupCallView: FC<Props> = ({
|
||||
);
|
||||
|
||||
let body: ReactNode;
|
||||
if (isJoined) {
|
||||
if (enterRTCError) {
|
||||
// If an ElementCallError was recorded, then create a component that will fail to render and throw
|
||||
// an ElementCallRichError error. This will then be handled by the ErrorBoundary component.
|
||||
const ErrorComponent = (): ReactNode => {
|
||||
throw new ElementCallRichError(enterRTCError);
|
||||
};
|
||||
body = <ErrorComponent />;
|
||||
} else if (isJoined) {
|
||||
body = (
|
||||
<>
|
||||
{shareModal}
|
||||
|
||||
@@ -13,6 +13,7 @@ import EventEmitter from "events";
|
||||
import { enterRTCSession, leaveRTCSession } from "../src/rtcSessionHelpers";
|
||||
import { mockConfig } from "./utils/test";
|
||||
import { ElementWidgetActions, widget } from "./widget";
|
||||
import { ErrorCode } from "./utils/ec-errors.ts";
|
||||
|
||||
const actualWidget = await vi.hoisted(async () => vi.importActual("./widget"));
|
||||
vi.mock("./widget", () => ({
|
||||
@@ -137,3 +138,50 @@ test("leaveRTCSession doesn't close the widget on a fatal error", async () => {
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
test("It fails with configuration error if no live kit url config is set in fallback", async () => {
|
||||
mockConfig({});
|
||||
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
|
||||
|
||||
const mockedSession = vi.mocked({
|
||||
room: {
|
||||
roomId: "roomId",
|
||||
client: {
|
||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
getFocusInUse: vi.fn(),
|
||||
joinRoomSession: vi.fn(),
|
||||
}) as unknown as MatrixRTCSession;
|
||||
|
||||
await expect(enterRTCSession(mockedSession, false)).rejects.toThrowError(
|
||||
expect.objectContaining({ code: ErrorCode.MISSING_LIVE_KIT_SERVICE_URL }),
|
||||
);
|
||||
});
|
||||
|
||||
test("It should not fail with configuration error if homeserver config has livekit url but not fallback", async () => {
|
||||
mockConfig({});
|
||||
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
type: "livekit",
|
||||
livekit_service_url: "http://my-well-known-service-url.com",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const mockedSession = vi.mocked({
|
||||
room: {
|
||||
roomId: "roomId",
|
||||
client: {
|
||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
getFocusInUse: vi.fn(),
|
||||
joinRoomSession: vi.fn(),
|
||||
}) as unknown as MatrixRTCSession;
|
||||
|
||||
await enterRTCSession(mockedSession, false);
|
||||
});
|
||||
|
||||
@@ -8,16 +8,17 @@ Please see LICENSE in the repository root for full details.
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
type LivekitFocus,
|
||||
type LivekitFocusActive,
|
||||
isLivekitFocus,
|
||||
isLivekitFocusConfig,
|
||||
type LivekitFocus,
|
||||
type LivekitFocusActive,
|
||||
} from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
|
||||
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
|
||||
|
||||
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||
import { Config } from "./config/Config";
|
||||
import { ElementWidgetActions, type WidgetHelpers, widget } from "./widget";
|
||||
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
|
||||
import { ElementCallError, ErrorCode } from "./utils/ec-errors.ts";
|
||||
|
||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
||||
|
||||
@@ -80,9 +81,10 @@ async function makePreferredLivekitFoci(
|
||||
}
|
||||
|
||||
if (preferredFoci.length === 0)
|
||||
throw new Error(
|
||||
throw new ElementCallError(
|
||||
`No livekit_service_url is configured so we could not create a focus.
|
||||
Currently we skip computing a focus based on other users in the room.`,
|
||||
ErrorCode.MISSING_LIVE_KIT_SERVICE_URL,
|
||||
);
|
||||
return Promise.resolve(preferredFoci);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user