* Add media hints for notification events. * Prevent showing calling view when disconnected from Livekit. (#3491) * Refactor disconnection handling * Use "unknown" * Update signature * Add tests * Expose livekitConnectionState directly * fix whoopsie * Update dependency livekit-client to v2.15.7 (#3496) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix the interactivity of buttons while reconnecting or in earpiece mode (#3486) * Fix the interactivity of buttons while reconnecting or in earpiece mode When we're in one of these modes, we need to ensure that everything above the overlay (the header and footer buttons) is interactive, while everything obscured by the overlay (the media tiles) is non-interactive and removed from the accessibility tree. It's not a very easy task to trap focus *outside* an element, so the best solution I could come up with is to set tabindex="-1" manually on all interactive elements belonging to the media tiles. * Write a Playwright test for reconnecting * fix lints Signed-off-by: Timo K <toger5@hotmail.de> * fix test Signed-off-by: Timo K <toger5@hotmail.de> * enable http2 for matrx-rtc host to allow the jwt service to talk to the SFU * remove rate limit for delayed events * more time to connect to livekit SFU * Due to a Firefox issue we set the start anchor for the tab test to the Mute microphone button * adapt to most recent Element Web version * Use the "End call" button as proofe for a started call * Currrenty disabled due to recent Element Web - not indicating the number of participants - bypassing Lobby * linting * disable 'can only interact with header and footer while reconnecting' for firefox --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: Timo <16718859+toger5@users.noreply.github.com> Co-authored-by: Timo K <toger5@hotmail.de> Co-authored-by: fkwp <github-fkwp@w4ve.de> * Log when a track is unpublished or runs into an error (#3495) * default mute states (unmuted!) in widget mode (embedded + intent) (#3494) * default mute states (unmuted!) in widget mode (embedded + intent) Signed-off-by: Timo K <toger5@hotmail.de> * review Signed-off-by: Timo K <toger5@hotmail.de> * introduce a cache for the url params. Signed-off-by: Timo K <toger5@hotmail.de> * Add an option to skip the cache. Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de> * Apply new hint code * missed a bit * fix intent * Automatically update intent on mute change * update packages * lint * Fix tests * fix merge fails --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Robin <robin@robin.town> Co-authored-by: Timo <16718859+toger5@users.noreply.github.com> Co-authored-by: Timo K <toger5@hotmail.de> Co-authored-by: fkwp <github-fkwp@w4ve.de>
202 lines
6.9 KiB
TypeScript
202 lines
6.9 KiB
TypeScript
/*
|
|
Copyright 2023, 2024 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 {
|
|
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";
|
|
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";
|
|
|
|
export function makeActiveFocus(): LivekitFocusActive {
|
|
return {
|
|
type: "livekit",
|
|
focus_selection: "oldest_membership",
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// 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) {
|
|
// 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)) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
|
if (urlFromConf) {
|
|
const focusFormConf: LivekitFocus = {
|
|
type: "livekit",
|
|
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);
|
|
|
|
// TODO: we want to do something like this:
|
|
//
|
|
// const focusOtherMembers = await focusFromOtherMembers(
|
|
// rtcSession,
|
|
// livekitAlias,
|
|
// );
|
|
// if (focusOtherMembers) preferredFoci.push(focusOtherMembers);
|
|
}
|
|
|
|
export async function enterRTCSession(
|
|
rtcSession: MatrixRTCSession,
|
|
encryptMedia: boolean,
|
|
useNewMembershipManager = true,
|
|
useExperimentalToDeviceTransport = false,
|
|
): Promise<void> {
|
|
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.
|
|
// groupCallOTelMembership?.onJoinCall();
|
|
|
|
// right now we assume everything is a room-scoped call
|
|
const livekitAlias = rtcSession.room.roomId;
|
|
const { features, matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
|
const useDeviceSessionMemberEvents =
|
|
features?.feature_use_device_session_member_events;
|
|
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
|
rtcSession.joinRoomSession(
|
|
await makePreferredLivekitFoci(rtcSession, livekitAlias),
|
|
makeActiveFocus(),
|
|
{
|
|
notificationType,
|
|
callIntent,
|
|
useNewMembershipManager,
|
|
manageMediaKeys: encryptMedia,
|
|
...(useDeviceSessionMemberEvents !== undefined && {
|
|
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
|
}),
|
|
delayedLeaveEventRestartMs:
|
|
matrixRtcSessionConfig?.delayed_leave_event_restart_ms,
|
|
delayedLeaveEventDelayMs:
|
|
matrixRtcSessionConfig?.delayed_leave_event_delay_ms,
|
|
delayedLeaveEventRestartLocalTimeoutMs:
|
|
matrixRtcSessionConfig?.delayed_leave_event_restart_local_timeout_ms,
|
|
networkErrorRetryMs: matrixRtcSessionConfig?.network_error_retry_ms,
|
|
makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms,
|
|
membershipEventExpiryMs:
|
|
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
|
useExperimentalToDeviceTransport,
|
|
},
|
|
);
|
|
if (widget) {
|
|
try {
|
|
await widget.api.transport.send(ElementWidgetActions.JoinCall, {});
|
|
} catch (e) {
|
|
logger.error("Failed to send join action", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
const widgetPostHangupProcedure = async (
|
|
widget: WidgetHelpers,
|
|
cause: "user" | "error",
|
|
promiseBeforeHangup?: Promise<unknown>,
|
|
): Promise<void> => {
|
|
try {
|
|
await widget.api.setAlwaysOnScreen(false);
|
|
} catch (e) {
|
|
logger.error("Failed to set call widget `alwaysOnScreen` to false", e);
|
|
}
|
|
|
|
// Wait for any last bits before hanging up.
|
|
await promiseBeforeHangup;
|
|
// 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.
|
|
try {
|
|
await widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
|
} catch (e) {
|
|
logger.error("Failed to send hangup action", e);
|
|
}
|
|
// 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.
|
|
if (cause === "user" && !getUrlParams().returnToLobby) {
|
|
try {
|
|
await widget.api.transport.send(ElementWidgetActions.Close, {});
|
|
} catch (e) {
|
|
logger.error("Failed to send close action", e);
|
|
}
|
|
widget.api.transport.stop();
|
|
}
|
|
};
|
|
|
|
export async function leaveRTCSession(
|
|
rtcSession: MatrixRTCSession,
|
|
cause: "user" | "error",
|
|
promiseBeforeHangup?: Promise<unknown>,
|
|
): Promise<void> {
|
|
await rtcSession.leaveRoomSession();
|
|
if (widget) {
|
|
await widgetPostHangupProcedure(widget, cause, promiseBeforeHangup);
|
|
} else {
|
|
await promiseBeforeHangup;
|
|
}
|
|
}
|