2023-08-18 09:03:21 +01:00
|
|
|
/*
|
2024-09-06 10:22:13 +02:00
|
|
|
Copyright 2023, 2024 New Vector Ltd.
|
2023-08-18 09:03:21 +01:00
|
|
|
|
2025-02-18 17:59:58 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-06 10:22:13 +02:00
|
|
|
Please see LICENSE in the repository root for full details.
|
2023-08-18 09:03:21 +01:00
|
|
|
*/
|
|
|
|
|
|
2024-06-19 16:41:52 +02:00
|
|
|
import {
|
|
|
|
|
isLivekitFocus,
|
|
|
|
|
isLivekitFocusConfig,
|
2025-02-26 14:55:03 +01:00
|
|
|
type LivekitFocus,
|
|
|
|
|
type LivekitFocusActive,
|
2025-08-05 15:13:04 +02:00
|
|
|
type MatrixRTCSession,
|
2025-03-13 13:58:43 +01:00
|
|
|
} from "matrix-js-sdk/lib/matrixrtc";
|
2025-08-05 15:13:04 +02:00
|
|
|
import { logger } from "matrix-js-sdk/lib/logger";
|
2025-03-13 13:58:43 +01:00
|
|
|
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
2023-08-18 09:03:21 +01:00
|
|
|
|
|
|
|
|
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
|
|
|
|
import { Config } from "./config/Config";
|
2025-02-26 14:55:03 +01:00
|
|
|
import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
|
2025-07-18 10:58:50 -04:00
|
|
|
import { MatrixRTCFocusMissingError } from "./utils/errors";
|
|
|
|
|
import { getUrlParams } from "./UrlParams";
|
2025-08-05 15:13:04 +02:00
|
|
|
import { getSFUConfigWithOpenID } from "./livekit/openIDSFU.ts";
|
2023-08-18 09:03:21 +01:00
|
|
|
|
2024-06-19 16:41:52 +02:00
|
|
|
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
2023-08-18 09:03:21 +01:00
|
|
|
|
2024-06-19 16:41:52 +02:00
|
|
|
export function makeActiveFocus(): LivekitFocusActive {
|
2023-08-18 09:03:21 +01:00
|
|
|
return {
|
|
|
|
|
type: "livekit",
|
2024-06-19 16:41:52 +02:00
|
|
|
focus_selection: "oldest_membership",
|
2023-08-18 09:03:21 +01:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 16:41:52 +02:00
|
|
|
async function makePreferredLivekitFoci(
|
|
|
|
|
rtcSession: MatrixRTCSession,
|
|
|
|
|
livekitAlias: string,
|
|
|
|
|
): Promise<LivekitFocus[]> {
|
|
|
|
|
logger.log("Start building foci_preferred list: ", rtcSession.room.roomId);
|
|
|
|
|
|
|
|
|
|
const preferredFoci: LivekitFocus[] = [];
|
|
|
|
|
|
|
|
|
|
// Make the Focus from the running rtc session the highest priority one
|
|
|
|
|
// This minimizes how often we need to switch foci during a call.
|
|
|
|
|
const focusInUse = rtcSession.getFocusInUse();
|
|
|
|
|
if (focusInUse && isLivekitFocus(focusInUse)) {
|
|
|
|
|
logger.log("Adding livekit focus from oldest member: ", focusInUse);
|
|
|
|
|
preferredFoci.push(focusInUse);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-17 16:44:50 +00:00
|
|
|
// Prioritize the .well-known/matrix/client, if available, over the configured SFU
|
|
|
|
|
const domain = rtcSession.room.client.getDomain();
|
|
|
|
|
if (domain) {
|
|
|
|
|
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
|
|
|
|
// been fully configured and started
|
2025-08-05 15:13:04 +02:00
|
|
|
const wellKnownFoci = await getFocusListFromWellKnown(domain, livekitAlias);
|
|
|
|
|
logger.log("Adding livekit focus from well known: ", wellKnownFoci);
|
|
|
|
|
preferredFoci.push(...wellKnownFoci);
|
2024-06-19 16:41:52 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-05 15:13:04 +02:00
|
|
|
const focusFormConf = getFocusListFromConfig(livekitAlias);
|
|
|
|
|
if (focusFormConf) {
|
2024-06-19 16:41:52 +02:00
|
|
|
logger.log("Adding livekit focus from config: ", focusFormConf);
|
|
|
|
|
preferredFoci.push(focusFormConf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (preferredFoci.length === 0)
|
2025-02-27 09:26:38 +01:00
|
|
|
throw new MatrixRTCFocusMissingError(domain ?? "");
|
2024-09-10 09:49:35 +02:00
|
|
|
return Promise.resolve(preferredFoci);
|
2024-06-19 16:41:52 +02:00
|
|
|
|
|
|
|
|
// TODO: we want to do something like this:
|
|
|
|
|
//
|
|
|
|
|
// const focusOtherMembers = await focusFromOtherMembers(
|
|
|
|
|
// rtcSession,
|
|
|
|
|
// livekitAlias,
|
|
|
|
|
// );
|
|
|
|
|
// if (focusOtherMembers) preferredFoci.push(focusOtherMembers);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 15:13:04 +02:00
|
|
|
async function getFocusListFromWellKnown(
|
|
|
|
|
domain: string,
|
|
|
|
|
alias: string,
|
|
|
|
|
): Promise<LivekitFocus[]> {
|
|
|
|
|
if (domain) {
|
|
|
|
|
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
|
|
|
|
// been fully configured and started
|
|
|
|
|
const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[
|
|
|
|
|
FOCI_WK_KEY
|
|
|
|
|
];
|
|
|
|
|
if (Array.isArray(wellKnownFoci)) {
|
|
|
|
|
return wellKnownFoci
|
|
|
|
|
.filter((f) => !!f)
|
|
|
|
|
.filter(isLivekitFocusConfig)
|
|
|
|
|
.map((wellKnownFocus) => {
|
|
|
|
|
return { ...wellKnownFocus, livekit_alias: alias };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getFocusListFromConfig(livekitAlias: string): LivekitFocus | null {
|
|
|
|
|
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
|
|
|
|
if (urlFromConf) {
|
|
|
|
|
return {
|
|
|
|
|
type: "livekit",
|
|
|
|
|
livekit_service_url: urlFromConf,
|
|
|
|
|
livekit_alias: livekitAlias,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getMyPreferredLivekitFoci(
|
|
|
|
|
domain: string | null,
|
|
|
|
|
livekitAlias: string,
|
|
|
|
|
): Promise<LivekitFocus> {
|
|
|
|
|
if (domain) {
|
|
|
|
|
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
|
|
|
|
// been fully configured and started
|
|
|
|
|
const wellKnownFociList = await getFocusListFromWellKnown(
|
|
|
|
|
domain,
|
|
|
|
|
livekitAlias,
|
|
|
|
|
);
|
|
|
|
|
if (wellKnownFociList.length > 0) {
|
|
|
|
|
return wellKnownFociList[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
|
|
|
|
if (urlFromConf) {
|
|
|
|
|
return {
|
|
|
|
|
type: "livekit",
|
|
|
|
|
livekit_service_url: urlFromConf,
|
|
|
|
|
livekit_alias: livekitAlias,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
throw new MatrixRTCFocusMissingError(domain ?? "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop-gap solution for pre-warming the SFU.
|
|
|
|
|
// This is needed to ensure that the livekit room is created before we try to join the rtc session.
|
|
|
|
|
// This is because the livekit room creation is done by the auth service and this can be restricted to
|
|
|
|
|
// only specific users, so we need to ensure that the room is created before we try to join it.
|
|
|
|
|
async function preWarmSFU(rtcSession: MatrixRTCSession, livekitAlias: string) {
|
|
|
|
|
const client = rtcSession.room.client;
|
|
|
|
|
// We need to make sure that the livekit room is created before sending the membership event
|
|
|
|
|
// because other joiners might not be able to join the call if the room does not exist yet.
|
|
|
|
|
const fociToWarmup = await getMyPreferredLivekitFoci(
|
|
|
|
|
client.getDomain(),
|
2025-08-05 15:30:15 +02:00
|
|
|
livekitAlias,
|
2025-08-05 15:13:04 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Request a token in advance to warm up the livekit room.
|
|
|
|
|
// Let it throw if it fails, errors will be handled by the ErrorBoundary, if it fails now
|
|
|
|
|
// it will fail later when we try to join the room.
|
|
|
|
|
await getSFUConfigWithOpenID(client, fociToWarmup);
|
|
|
|
|
// For now we don't do anything with the token returned by `getSFUConfigWithOpenID`, it is just to ensure that we
|
|
|
|
|
// call the `sfu/get` endpoint so that the auth service create the room in advance if it can.
|
|
|
|
|
// Note: This is not actually checking that the room was created! If the roon creation is
|
|
|
|
|
// not done by the auth service, the call will fail later when we try to join the room; that case
|
|
|
|
|
// is a miss-configuration of the auth service, you should be able to create room in your selected SFU.
|
|
|
|
|
// A solution could be to call the internal `/validate` endpoint to check that the room exists, but this needs
|
|
|
|
|
// to access livekit internal APIs, so we don't do it for now.
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 16:41:52 +02:00
|
|
|
export async function enterRTCSession(
|
2023-10-16 17:45:06 +01:00
|
|
|
rtcSession: MatrixRTCSession,
|
|
|
|
|
encryptMedia: boolean,
|
2025-03-07 17:27:04 +01:00
|
|
|
useNewMembershipManager = true,
|
2025-04-11 10:07:50 +02:00
|
|
|
useExperimentalToDeviceTransport = false,
|
2024-06-19 16:41:52 +02:00
|
|
|
): Promise<void> {
|
2023-08-18 09:03:21 +01:00
|
|
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
|
|
|
|
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
|
|
|
|
|
|
|
|
|
// This must be called before we start trying to join the call, as we need to
|
|
|
|
|
// have started tracking by the time calls start getting created.
|
2023-10-25 13:44:33 +02:00
|
|
|
// groupCallOTelMembership?.onJoinCall();
|
2023-08-18 09:03:21 +01:00
|
|
|
|
2023-10-10 14:14:39 +02:00
|
|
|
// right now we assume everything is a room-scoped call
|
2023-08-18 09:03:21 +01:00
|
|
|
const livekitAlias = rtcSession.room.roomId;
|
2024-11-11 16:53:37 +00:00
|
|
|
const { features, matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
2024-07-05 21:10:16 +09:00
|
|
|
const useDeviceSessionMemberEvents =
|
2024-11-11 16:53:37 +00:00
|
|
|
features?.feature_use_device_session_member_events;
|
2025-08-05 15:13:04 +02:00
|
|
|
|
|
|
|
|
// Pre-warm the SFU to ensure that the room is created before anyone tries to join it.
|
|
|
|
|
await preWarmSFU(rtcSession, livekitAlias);
|
|
|
|
|
|
2024-06-19 16:41:52 +02:00
|
|
|
rtcSession.joinRoomSession(
|
|
|
|
|
await makePreferredLivekitFoci(rtcSession, livekitAlias),
|
|
|
|
|
makeActiveFocus(),
|
2024-07-05 21:10:16 +09:00
|
|
|
{
|
2025-07-18 10:58:50 -04:00
|
|
|
notificationType: getUrlParams().sendNotificationType,
|
2025-03-07 17:27:04 +01:00
|
|
|
useNewMembershipManager,
|
2024-07-05 21:10:16 +09:00
|
|
|
manageMediaKeys: encryptMedia,
|
|
|
|
|
...(useDeviceSessionMemberEvents !== undefined && {
|
|
|
|
|
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
|
|
|
|
}),
|
2025-06-10 18:09:52 +02:00
|
|
|
delayedLeaveEventRestartMs:
|
2025-08-04 17:59:04 +02:00
|
|
|
matrixRtcSessionConfig?.delayed_leave_event_restart_ms,
|
2025-05-19 18:04:07 +02:00
|
|
|
delayedLeaveEventDelayMs:
|
2025-08-04 17:59:04 +02:00
|
|
|
matrixRtcSessionConfig?.delayed_leave_event_delay_ms,
|
2025-07-30 14:58:45 +02:00
|
|
|
delayedLeaveEventRestartLocalTimeoutMs:
|
|
|
|
|
matrixRtcSessionConfig?.delayed_leave_event_restart_local_timeout_ms,
|
2025-06-10 18:09:52 +02:00
|
|
|
networkErrorRetryMs: matrixRtcSessionConfig?.network_error_retry_ms,
|
2025-08-04 17:59:04 +02:00
|
|
|
makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms,
|
2025-06-10 18:09:52 +02:00
|
|
|
membershipEventExpiryMs:
|
|
|
|
|
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
2025-04-11 10:07:50 +02:00
|
|
|
useExperimentalToDeviceTransport,
|
2024-07-05 21:10:16 +09:00
|
|
|
},
|
2024-06-19 16:41:52 +02:00
|
|
|
);
|
2025-03-05 09:25:52 -05:00
|
|
|
if (widget) {
|
|
|
|
|
try {
|
|
|
|
|
await widget.api.transport.send(ElementWidgetActions.JoinCall, {});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error("Failed to send join action", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-18 09:03:21 +01:00
|
|
|
}
|
|
|
|
|
|
2023-10-30 17:30:30 +01:00
|
|
|
const widgetPostHangupProcedure = async (
|
|
|
|
|
widget: WidgetHelpers,
|
2025-02-17 19:19:31 +07:00
|
|
|
cause: "user" | "error",
|
2024-12-12 07:33:47 +00:00
|
|
|
promiseBeforeHangup?: Promise<unknown>,
|
2023-10-30 17:30:30 +01:00
|
|
|
): Promise<void> => {
|
2024-09-10 09:49:35 +02:00
|
|
|
try {
|
|
|
|
|
await widget.api.setAlwaysOnScreen(false);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error("Failed to set call widget `alwaysOnScreen` to false", e);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-12 07:33:47 +00:00
|
|
|
// Wait for any last bits before hanging up.
|
|
|
|
|
await promiseBeforeHangup;
|
2023-10-30 17:30:30 +01:00
|
|
|
// We send the hangup event after the memberships have been updated
|
|
|
|
|
// calling leaveRTCSession.
|
|
|
|
|
// We need to wait because this makes the client hosting this widget killing the IFrame.
|
2025-02-24 11:43:15 +07:00
|
|
|
try {
|
|
|
|
|
await widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error("Failed to send hangup action", e);
|
|
|
|
|
}
|
2025-02-17 19:19:31 +07:00
|
|
|
// On a normal user hangup we can shut down and close the widget. But if an
|
|
|
|
|
// error occurs we should keep the widget open until the user reads it.
|
2025-03-04 15:09:59 -05:00
|
|
|
if (cause === "user" && !getUrlParams().returnToLobby) {
|
2025-02-24 11:43:15 +07:00
|
|
|
try {
|
|
|
|
|
await widget.api.transport.send(ElementWidgetActions.Close, {});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error("Failed to send close action", e);
|
|
|
|
|
}
|
2025-02-17 19:19:31 +07:00
|
|
|
widget.api.transport.stop();
|
|
|
|
|
}
|
2023-10-30 17:30:30 +01:00
|
|
|
};
|
|
|
|
|
|
2023-10-04 18:27:07 +02:00
|
|
|
export async function leaveRTCSession(
|
2023-10-11 10:42:04 -04:00
|
|
|
rtcSession: MatrixRTCSession,
|
2025-02-17 19:19:31 +07:00
|
|
|
cause: "user" | "error",
|
2024-12-12 07:33:47 +00:00
|
|
|
promiseBeforeHangup?: Promise<unknown>,
|
2023-10-04 18:27:07 +02:00
|
|
|
): Promise<void> {
|
|
|
|
|
await rtcSession.leaveRoomSession();
|
2023-10-30 17:30:30 +01:00
|
|
|
if (widget) {
|
2025-02-17 19:19:31 +07:00
|
|
|
await widgetPostHangupProcedure(widget, cause, promiseBeforeHangup);
|
2024-12-12 07:33:47 +00:00
|
|
|
} else {
|
|
|
|
|
await promiseBeforeHangup;
|
2023-10-30 17:30:30 +01:00
|
|
|
}
|
2023-08-18 09:03:21 +01:00
|
|
|
}
|