WIP refactor for removing m.call events

This commit is contained in:
David Baker
2023-08-16 18:41:27 +01:00
parent e51492b3c7
commit 4242d45ba2
12 changed files with 209 additions and 323 deletions

View File

@@ -15,17 +15,13 @@ limitations under the License.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import { HTMLAttributes, ReactNode, useCallback } from "react"; import { HTMLAttributes, ReactNode } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Room } from "matrix-js-sdk/src/models/room";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./Header.module.css"; import styles from "./Header.module.css";
import { useModalTriggerState } from "./Modal";
import { Button } from "./button";
import { ReactComponent as Logo } from "./icons/Logo.svg"; import { ReactComponent as Logo } from "./icons/Logo.svg";
import { Subtitle } from "./typography/Typography"; import { Subtitle } from "./typography/Typography";
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
interface HeaderProps extends HTMLAttributes<HTMLElement> { interface HeaderProps extends HTMLAttributes<HTMLElement> {
children: ReactNode; children: ReactNode;
@@ -125,34 +121,3 @@ export function RoomHeaderInfo({ roomName }: RoomHeaderInfo) {
</> </>
); );
} }
interface VersionMismatchWarningProps {
users: Set<string>;
room: Room;
}
export function VersionMismatchWarning({
users,
room,
}: VersionMismatchWarningProps) {
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const onDetailsClick = useCallback(() => {
modalState.open();
}, [modalState]);
if (users.size === 0) return null;
return (
<span className={styles.versionMismatchWarning}>
{t("Incompatible versions!")}
<Button variant="link" onClick={onDetailsClick}>
{t("Details")}
</Button>
{modalState.isOpen && (
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
)}
</span>
);
}

View File

@@ -1,60 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { FC, useMemo } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Modal, ModalContent } from "./Modal";
import { Body } from "./typography/Typography";
interface Props {
userIds: Set<string>;
room: Room;
onClose: () => void;
}
export const IncompatibleVersionModal: FC<Props> = ({
userIds,
room,
onClose,
...rest
}) => {
const { t } = useTranslation();
const userLis = useMemo(
() => [...userIds].map((u) => <li>{room.getMember(u)?.name ?? u}</li>),
[userIds, room]
);
return (
<Modal
title={t("Incompatible versions")}
isDismissable
onClose={onClose}
{...rest}
>
<ModalContent>
<Body>
<Trans>
Other users are trying to join this call from incompatible versions.
These users should ensure that they have refreshed their browsers:
<ul>{userLis}</ul>
</Trans>
</Body>
</ModalContent>
</Modal>
);
};

View File

@@ -172,7 +172,6 @@ export async function initClient(
localTimeoutMs: 5000, localTimeoutMs: 5000,
useE2eForGroupCall: e2eEnabled, useE2eForGroupCall: e2eEnabled,
fallbackICEServerAllowed: fallbackICEServerAllowed, fallbackICEServerAllowed: fallbackICEServerAllowed,
useLivekitForGroupCalls: true,
}); });
try { try {

View File

@@ -16,8 +16,8 @@ limitations under the License.
import { ReactNode } from "react"; import { ReactNode } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useLoadGroupCall } from "./useLoadGroupCall"; import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
@@ -26,7 +26,7 @@ interface Props {
client: MatrixClient; client: MatrixClient;
roomIdOrAlias: string; roomIdOrAlias: string;
viaServers: string[]; viaServers: string[];
children: (groupCall: GroupCall) => ReactNode; children: (rtcSession: MatrixRTCSession) => ReactNode;
createPtt: boolean; createPtt: boolean;
} }
@@ -53,7 +53,7 @@ export function GroupCallLoader({
</FullScreenView> </FullScreenView>
); );
case "loaded": case "loaded":
return <>{children(groupCallState.groupCall)}</>; return <>{children(groupCallState.rtcSession)}</>;
case "failed": case "failed":
return <ErrorView error={groupCallState.error} />; return <ErrorView error={groupCallState.error} />;
} }

View File

@@ -16,33 +16,35 @@ limitations under the License.
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Room } from "livekit-client"; import { Room } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { Focus } from "matrix-js-sdk/src/matrixrtc/focus";
import type { IWidgetApiRequest } from "matrix-widget-api"; import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget"; import { widget, ElementWidgetActions, JoinCallData } from "../widget";
import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView"; import { LobbyView } from "./LobbyView";
import { MatrixInfo } from "./VideoPreview"; import { MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView"; import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import { E2EEConfig } from "../livekit/useLiveKit"; import { E2EEConfig } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils"; import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader"; //import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView"; import { ActiveCall } from "./InCallView";
import { Config } from "../config/Config";
import { MuteStates, useMuteStates } from "./MuteStates"; import { MuteStates, useMuteStates } from "./MuteStates";
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext"; import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
import { LivekitFocus } from "../livekit/LivekitFocus";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
declare global { declare global {
interface Window { interface Window {
groupCall?: GroupCall; rtcSession?: MatrixRTCSession;
} }
} }
@@ -52,7 +54,7 @@ interface Props {
isEmbedded: boolean; isEmbedded: boolean;
preload: boolean; preload: boolean;
hideHeader: boolean; hideHeader: boolean;
groupCall: GroupCall; rtcSession: MatrixRTCSession;
} }
export function GroupCallView({ export function GroupCallView({
@@ -61,9 +63,9 @@ export function GroupCallView({
isEmbedded, isEmbedded,
preload, preload,
hideHeader, hideHeader,
groupCall, rtcSession,
}: Props) { }: Props) {
const { /*const {
state, state,
error, error,
enter, enter,
@@ -71,33 +73,36 @@ export function GroupCallView({
participants, participants,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
otelGroupCallMembership, otelGroupCallMembership,
} = useGroupCall(groupCall, client); } = useGroupCall(groupCall, client);*/
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
window.groupCall = groupCall; window.rtcSession = rtcSession;
return () => { return () => {
delete window.groupCall; delete window.rtcSession;
}; };
}, [groupCall]); }, [rtcSession]);
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const matrixInfo = useMemo((): MatrixInfo => { const matrixInfo = useMemo((): MatrixInfo => {
return { return {
displayName: displayName!, displayName: displayName!,
avatarUrl: avatarUrl!, avatarUrl: avatarUrl!,
roomId: groupCall.room.roomId, roomId: rtcSession.room.roomId,
roomName: groupCall.room.name, roomName: rtcSession.room.name,
roomAlias: groupCall.room.getCanonicalAlias(), roomAlias: rtcSession.room.getCanonicalAlias(),
}; };
}, [displayName, avatarUrl, groupCall]); }, [displayName, avatarUrl, rtcSession]);
const deviceContext = useMediaDevices(); const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices>(); const latestDevices = useRef<MediaDevices>();
latestDevices.current = deviceContext; latestDevices.current = deviceContext;
const muteStates = useMuteStates(participants.size); const muteStates = useMuteStates(memberships.length);
const latestMuteStates = useRef<MuteStates>(); const latestMuteStates = useRef<MuteStates>();
latestMuteStates.current = muteStates; latestMuteStates.current = muteStates;
@@ -154,10 +159,13 @@ export function GroupCallView({
} }
} }
await enter(); enterRTCSession(rtcSession);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); // we only have room sessions right now, so call ID is the emprty string - we use the room ID
PosthogAnalytics.instance.eventCallStarted.track(
rtcSession.room.roomId
);
await Promise.all([ await Promise.all([
widget!.api.setAlwaysOnScreen(true), widget!.api.setAlwaysOnScreen(true),
@@ -170,19 +178,18 @@ export function GroupCallView({
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
}; };
} }
}, [groupCall, preload, enter]); }, [rtcSession, preload]);
useEffect(() => { useEffect(() => {
if (isEmbedded && !preload) { if (isEmbedded && !preload) {
// In embedded mode, bypass the lobby and just enter the call straight away // In embedded mode, bypass the lobby and just enter the call straight away
enter(); enterRTCSession(rtcSession);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); // use the room ID as above
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
} }
}, [groupCall, isEmbedded, preload, enter]); }, [rtcSession, isEmbedded, preload]);
useSentryGroupCallHandler(groupCall);
const [left, setLeft] = useState(false); const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined); const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
@@ -193,21 +200,16 @@ export function GroupCallView({
setLeaveError(leaveError); setLeaveError(leaveError);
setLeft(true); setLeft(true);
let participantCount = 0;
for (const deviceMap of groupCall.participants.values()) {
participantCount += deviceMap.size;
}
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
// therefore we want the event to be sent instantly without getting queued/batched. // therefore we want the event to be sent instantly without getting queued/batched.
const sendInstantly = !!widget; const sendInstantly = !!widget;
PosthogAnalytics.instance.eventCallEnded.track( PosthogAnalytics.instance.eventCallEnded.track(
groupCall.groupCallId, rtcSession.room.roomId,
participantCount, rtcSession.memberships.length,
sendInstantly sendInstantly
); );
leave(); leaveRTCSession(rtcSession);
if (widget) { if (widget) {
// we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked. // we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked.
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
@@ -224,13 +226,13 @@ export function GroupCallView({
history.push("/"); history.push("/");
} }
}, },
[groupCall, leave, isPasswordlessUser, isEmbedded, history] [rtcSession, isPasswordlessUser, isEmbedded, history]
); );
useEffect(() => { useEffect(() => {
if (widget && state === GroupCallState.Entered) { if (widget && isJoined) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => { const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leave(); leaveRTCSession(rtcSession);
await widget!.api.transport.reply(ev.detail, {}); await widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false); widget!.api.setAlwaysOnScreen(false);
}; };
@@ -239,7 +241,7 @@ export function GroupCallView({
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup); widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
}; };
} }
}, [groupCall, state, leave]); }, [isJoined, rtcSession]);
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>( const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
undefined undefined
@@ -248,36 +250,40 @@ export function GroupCallView({
const onReconnect = useCallback(() => { const onReconnect = useCallback(() => {
setLeft(false); setLeft(false);
setLeaveError(undefined); setLeaveError(undefined);
groupCall.enter(); rtcSession.joinRoomSession();
}, [groupCall]); }, [rtcSession]);
const livekitServiceURL = const focus: Focus | undefined = rtcSession
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url; .getOldestMembership()
if (!livekitServiceURL) { ?.getActiveFoci()?.[0];
return <ErrorView error={new Error("No livekit_service_url defined")} />; if (
!focus ||
focus.type !== "livekit" ||
!(focus as LivekitFocus).livekit_alias ||
!(focus as LivekitFocus).livekit_service_url
) {
logger.error("Incompatible focus on call", focus);
return <ErrorView error={new Error("Call focus is not compatible!")} />;
} }
if (error) { if (isJoined) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
return ( return (
<OpenIDLoader /*<OpenIDLoader
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`} roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
> >*/
<ActiveCall <ActiveCall
client={client} client={client}
groupCall={groupCall} rtcSession={rtcSession}
participants={participants} memberships={memberships}
onLeave={onLeave} onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader}
hideHeader={hideHeader} muteStates={muteStates}
muteStates={muteStates} e2eeConfig={e2eeConfig}
e2eeConfig={e2eeConfig} //otelGroupCallMembership={otelGroupCallMembership}
otelGroupCallMembership={otelGroupCallMembership} />
/> //</OpenIDLoader>
</OpenIDLoader>
); );
} else if (left) { } else if (left) {
// The call ended view is shown for two reasons: prompting guests to create // The call ended view is shown for two reasons: prompting guests to create
@@ -293,7 +299,7 @@ export function GroupCallView({
) { ) {
return ( return (
<CallEndedView <CallEndedView
endedCallId={groupCall.groupCallId} endedCallId={rtcSession.room.roomId}
client={client} client={client}
isPasswordlessUser={isPasswordlessUser} isPasswordlessUser={isPasswordlessUser}
leaveError={leaveError} leaveError={leaveError}
@@ -321,7 +327,7 @@ export function GroupCallView({
muteStates={muteStates} muteStates={muteStates}
onEnter={(e2eeConfig?: E2EEConfig) => { onEnter={(e2eeConfig?: E2EEConfig) => {
setE2EEConfig(e2eeConfig); setE2EEConfig(e2eeConfig);
enter(); enterRTCSession(rtcSession);
}} }}
isEmbedded={isEmbedded} isEmbedded={isEmbedded}
hideHeader={hideHeader} hideHeader={hideHeader}

View File

@@ -27,7 +27,6 @@ import classNames from "classnames";
import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client"; import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { Ref, useCallback, useEffect, useMemo, useRef } from "react"; import { Ref, useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
@@ -35,6 +34,8 @@ import { OverlayTriggerState } from "@react-stately/overlays";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { RoomEventCallbacks } from "livekit-client/dist/src/room/Room"; import { RoomEventCallbacks } from "livekit-client/dist/src/room/Room";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
import type { IWidgetApiRequest } from "matrix-widget-api"; import type { IWidgetApiRequest } from "matrix-widget-api";
import { import {
@@ -45,22 +46,13 @@ import {
SettingsButton, SettingsButton,
InviteButton, InviteButton,
} from "../button"; } from "../button";
import { import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
Header,
LeftNav,
RightNav,
RoomHeaderInfo,
VersionMismatchWarning,
} from "../Header";
import { import {
useVideoGridLayout, useVideoGridLayout,
TileDescriptor, TileDescriptor,
VideoGrid, VideoGrid,
} from "../video-grid/VideoGrid"; } from "../video-grid/VideoGrid";
import { import { useShowConnectionStats } from "../settings/useSetting";
useShowInspector,
useShowConnectionStats,
} from "../settings/useSetting";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
@@ -68,10 +60,8 @@ import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion"; import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ElementWidgetActions, widget } from "../widget"; import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu"; import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css"; import styles from "./InCallView.module.css";
import { useJoinRule } from "./useJoinRule"; import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile"; import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid"; import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
@@ -120,24 +110,22 @@ export function ActiveCall(props: ActiveCallProps) {
export interface InCallViewProps { export interface InCallViewProps {
client: MatrixClient; client: MatrixClient;
groupCall: GroupCall; rtcSession: MatrixRTCSession;
livekitRoom: Room; livekitRoom: Room;
muteStates: MuteStates; muteStates: MuteStates;
participants: Map<RoomMember, Map<string, ParticipantInfo>>; memberships: CallMembership[];
onLeave: (error?: Error) => void; onLeave: (error?: Error) => void;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean; hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership; otelGroupCallMembership?: OTelGroupCallMembership;
} }
export function InCallView({ export function InCallView({
client, client,
groupCall, rtcSession,
livekitRoom, livekitRoom,
muteStates, muteStates,
participants, memberships,
onLeave, onLeave,
unencryptedEventsFromUsers,
hideHeader, hideHeader,
otelGroupCallMembership, otelGroupCallMembership,
}: InCallViewProps) { }: InCallViewProps) {
@@ -161,7 +149,7 @@ export function InCallView({
screenSharingTracks.length > 0 screenSharingTracks.length > 0
); );
const [showInspector] = useShowInspector(); //const [showInspector] = useShowInspector();
const [showConnectionStats] = useShowConnectionStats(); const [showConnectionStats] = useShowConnectionStats();
const { hideScreensharing } = useUrlParams(); const { hideScreensharing } = useUrlParams();
@@ -179,7 +167,7 @@ export function InCallView({
[muteStates] [muteStates]
); );
const joinRule = useJoinRule(groupCall.room); const joinRule = useJoinRule(rtcSession.room);
// This function incorrectly assumes that there is a camera and microphone, which is not always the case. // This function incorrectly assumes that there is a camera and microphone, which is not always the case.
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability! // TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
@@ -250,7 +238,7 @@ export function InCallView({
const reducedControls = boundsValid && bounds.width <= 400; const reducedControls = boundsValid && bounds.width <= 400;
const noControls = reducedControls && bounds.height <= 400; const noControls = reducedControls && bounds.height <= 400;
const items = useParticipantTiles(livekitRoom, participants); const items = useParticipantTiles(livekitRoom, memberships);
const { fullscreenItem, toggleFullscreen, exitFullscreen } = const { fullscreenItem, toggleFullscreen, exitFullscreen } =
useFullscreen(items); useFullscreen(items);
@@ -324,7 +312,7 @@ export function InCallView({
const { const {
modalState: rageshakeRequestModalState, modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps, modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId); } = useRageshakeRequestModal(rtcSession.room.roomId);
const { const {
modalState: settingsModalState, modalState: settingsModalState,
@@ -419,11 +407,7 @@ export function InCallView({
{!hideHeader && maximisedParticipant === null && ( {!hideHeader && maximisedParticipant === null && (
<Header> <Header>
<LeftNav> <LeftNav>
<RoomHeaderInfo roomName={groupCall.room.name} /> <RoomHeaderInfo roomName={rtcSession.room.name} />
<VersionMismatchWarning
users={unencryptedEventsFromUsers}
room={groupCall.room}
/>
<E2EELock /> <E2EELock />
</LeftNav> </LeftNav>
<RightNav> <RightNav>
@@ -439,31 +423,31 @@ export function InCallView({
{renderContent()} {renderContent()}
{footer} {footer}
</div> </div>
{otelGroupCallMembership && ( {/*otelGroupCallMembership && (
<GroupCallInspector <GroupCallInspector
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership} otelGroupCallMembership={otelGroupCallMembership}
show={showInspector} show={showInspector}
/> />
)} )*/}
{rageshakeRequestModalState.isOpen && !noControls && ( {rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal <RageshakeRequestModal
{...rageshakeRequestModalProps} {...rageshakeRequestModalProps}
roomId={groupCall.room.roomId} roomId={rtcSession.room.roomId}
/> />
)} )}
{settingsModalState.isOpen && ( {settingsModalState.isOpen && (
<SettingsModal <SettingsModal
client={client} client={client}
roomId={groupCall.room.roomId} roomId={rtcSession.room.roomId}
{...settingsModalProps} {...settingsModalProps}
/> />
)} )}
{inviteModalState.isOpen && ( {inviteModalState.isOpen && (
<InviteModal <InviteModal
roomIdOrAlias={ roomIdOrAlias={
groupCall.room.getCanonicalAlias() ?? groupCall.room.roomId rtcSession.room.getCanonicalAlias() ?? rtcSession.room.roomId
} }
{...inviteModalProps} {...inviteModalProps}
/> />
@@ -474,22 +458,26 @@ export function InCallView({
function useParticipantTiles( function useParticipantTiles(
livekitRoom: Room, livekitRoom: Room,
participants: Map<RoomMember, Map<string, ParticipantInfo>> memberships: CallMembership[]
): TileDescriptor<ItemData>[] { ): TileDescriptor<ItemData>[] {
const sfuParticipants = useParticipants({ const sfuParticipants = useParticipants({
room: livekitRoom, room: livekitRoom,
}); });
const items = useMemo(() => { const items = useMemo(() => {
// The IDs of the participants who published membership event to the room (i.e. are present from Matrix perspective).
const matrixParticipants: Map<string, RoomMember> = new Map( const matrixParticipants: Map<string, RoomMember> = new Map(
memberships.map((m) => [`${m.member.userId}:${m.deviceId}`, m.member])
);
// The IDs of the participants who published membership event to the room (i.e. are present from Matrix perspective).
/*const matrixParticipants: Map<string, RoomMember> = new Map(
[...participants.entries()].flatMap(([user, devicesMap]) => { [...participants.entries()].flatMap(([user, devicesMap]) => {
return [...devicesMap.keys()].map((deviceId) => [ return [...devicesMap.keys()].map((deviceId) => [
`${user.userId}:${deviceId}`, `${user.userId}:${deviceId}`,
user, user,
]); ]);
}) })
); );*/
const hasPresenter = const hasPresenter =
sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined; sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined;
@@ -558,7 +546,7 @@ function useParticipantTiles(
// If every item is a ghost, that probably means we're still connecting and // If every item is a ghost, that probably means we're still connecting and
// shouldn't bother showing anything yet // shouldn't bother showing anything yet
return allGhosts ? [] : tiles; return allGhosts ? [] : tiles;
}, [participants, sfuParticipants]); }, [memberships, sfuParticipants]);
return items; return items;
} }

View File

@@ -15,8 +15,8 @@ limitations under the License.
*/ */
import { FC, useEffect, useState, useCallback } from "react"; import { FC, useEffect, useState, useCallback } from "react";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useClientLegacy } from "../ClientContext"; import { useClientLegacy } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
@@ -73,10 +73,10 @@ export const RoomPage: FC = () => {
]); ]);
const groupCallView = useCallback( const groupCallView = useCallback(
(groupCall: GroupCall) => ( (rtcSession: MatrixRTCSession) => (
<GroupCallView <GroupCallView
client={client!} client={client!}
groupCall={groupCall} rtcSession={rtcSession}
isPasswordlessUser={passwordlessUser} isPasswordlessUser={passwordlessUser}
isEmbedded={isEmbedded} isEmbedded={isEmbedded}
preload={preload} preload={preload}

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2022 New Vector Ltd Copyright 2022-2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import {
SummaryStatsReport, SummaryStatsReport,
CallFeedReport, CallFeedReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport"; } from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { usePageUnload } from "./usePageUnload"; import { usePageUnload } from "./usePageUnload";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
@@ -77,7 +78,6 @@ interface UseGroupCallReturnType {
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
participants: Map<RoomMember, Map<string, ParticipantInfo>>; participants: Map<RoomMember, Map<string, ParticipantInfo>>;
hasLocalParticipant: boolean; hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>;
otelGroupCallMembership?: OTelGroupCallMembership; otelGroupCallMembership?: OTelGroupCallMembership;
} }
@@ -103,14 +103,14 @@ interface State {
let groupCallOTelMembership: OTelGroupCallMembership | undefined; let groupCallOTelMembership: OTelGroupCallMembership | undefined;
let groupCallOTelMembershipGroupCallId: string; let groupCallOTelMembershipGroupCallId: string;
function getParticipants( /*function getParticipants(
groupCall: GroupCall rtcSession: MatrixRTCSession
): Map<RoomMember, Map<string, ParticipantInfo>> { ): Map<RoomMember, Map<string, ParticipantInfo>> {
const participants = new Map<RoomMember, Map<string, ParticipantInfo>>(); const participants = new Map<RoomMember, Map<string, ParticipantInfo>>();
for (const [member, participantsStateMap] of groupCall.participants) { for (const membership of rtcSession.memberships) {
const participantInfoMap = new Map<string, ParticipantInfo>(); const participantInfoMap = new Map<string, ParticipantInfo>();
participants.set(member, participantInfoMap); participants.set(membership.member, participantInfoMap);
for (const [deviceId, participant] of participantsStateMap) { for (const [deviceId, participant] of participantsStateMap) {
const feed = groupCall.userMediaFeeds.find( const feed = groupCall.userMediaFeeds.find(
@@ -141,10 +141,10 @@ function getParticipants(
} }
return participants; return participants;
} }*/
export function useGroupCall( export function useGroupCall(
groupCall: GroupCall, rtcSession: MatrixRTCSession,
client: MatrixClient client: MatrixClient
): UseGroupCallReturnType { ): UseGroupCallReturnType {
const [ const [
@@ -171,7 +171,7 @@ export function useGroupCall(
isScreensharing: false, isScreensharing: false,
screenshareFeeds: [], screenshareFeeds: [],
requestingScreenshare: false, requestingScreenshare: false,
participants: getParticipants(groupCall), participants: getParticipants(rtcSession),
hasLocalParticipant: false, hasLocalParticipant: false,
}); });

View File

@@ -15,28 +15,19 @@ limitations under the License.
*/ */
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import {
GroupCallType,
GroupCallIntent,
} from "matrix-js-sdk/src/webrtc/groupCall";
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { SyncState } from "matrix-js-sdk/src/sync"; import { SyncState } from "matrix-js-sdk/src/sync";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import type { Room } from "matrix-js-sdk/src/models/room"; import type { Room } from "matrix-js-sdk/src/models/room";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
import { translatedError } from "../TranslatedError";
import { widget } from "../widget";
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
export type GroupCallLoaded = { export type GroupCallLoaded = {
kind: "loaded"; kind: "loaded";
groupCall: GroupCall; rtcSession: MatrixRTCSession;
}; };
export type GroupCallLoadFailed = { export type GroupCallLoadFailed = {
@@ -115,61 +106,12 @@ export const useLoadGroupCall = (
} }
}; };
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => { const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
const room = await fetchOrCreateRoom(); const room = await fetchOrCreateRoom();
logger.debug(`Fetched / joined room ${roomIdOrAlias}`); logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
let groupCall = client.getGroupCallForRoom(room.roomId);
logger.debug("Got group call", groupCall?.groupCallId);
if (groupCall) { const rtcSession = client.matrixRTC.getRoomSession(room);
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS); return rtcSession;
return groupCall;
}
if (
!widget &&
room.currentState.mayClientSendStateEvent(
EventType.GroupCallPrefix,
client
)
) {
// The call doesn't exist, but we can create it
console.log(
`No call found in ${roomIdOrAlias}: creating ${
createPtt ? "PTT" : "video"
} call`
);
groupCall = await client.createGroupCall(
room.roomId,
createPtt ? GroupCallType.Voice : GroupCallType.Video,
createPtt,
GroupCallIntent.Room
);
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
return groupCall;
}
// We don't have permission to create the call, so all we can do is wait
// for one to come in
return new Promise((resolve, reject) => {
const onGroupCallIncoming = (groupCall: GroupCall) => {
if (groupCall?.room.roomId === room.roomId) {
clearTimeout(timeout);
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
client.off(
GroupCallEventHandlerEvent.Incoming,
onGroupCallIncoming
);
resolve(groupCall);
}
};
client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
const timeout = setTimeout(() => {
client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
reject(translatedError("Fetching group call timed out.", t));
}, 30000);
});
}; };
const waitForClientSyncing = async () => { const waitForClientSyncing = async () => {
@@ -192,7 +134,7 @@ export const useLoadGroupCall = (
waitForClientSyncing() waitForClientSyncing()
.then(fetchOrCreateGroupCall) .then(fetchOrCreateGroupCall)
.then((groupCall) => setState({ kind: "loaded", groupCall })) .then((rtcSession) => setState({ kind: "loaded", rtcSession }))
.catch((error) => setState({ kind: "failed", error })); .catch((error) => setState({ kind: "failed", error }));
}, [client, roomIdOrAlias, viaServers, createPtt, t]); }, [client, roomIdOrAlias, viaServers, createPtt, t]);

View File

@@ -1,46 +0,0 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect } from "react";
import * as Sentry from "@sentry/react";
import { GroupCall, GroupCallEvent } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
export function useSentryGroupCallHandler(groupCall: GroupCall) {
useEffect(() => {
function onHangup(call: MatrixCall) {
if (call.hangupReason === "ice_failed") {
Sentry.captureException(new Error("Call hangup due to ICE failure."));
}
}
function onError(error: Error) {
Sentry.captureException(error);
}
if (groupCall) {
groupCall.on(CallEvent.Hangup, onHangup);
groupCall.on(GroupCallEvent.Error, onError);
}
return () => {
if (groupCall) {
groupCall.removeListener(CallEvent.Hangup, onHangup);
groupCall.removeListener(GroupCallEvent.Error, onError);
}
};
}, [groupCall]);
}

View File

@@ -0,0 +1,44 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useCallback, useEffect, useState } from "react";
export function useMatrixRTCSessionJoinState(
rtcSession: MatrixRTCSession
): boolean {
const [isJoined, setJoined] = useState(rtcSession.isJoined());
const onJoinStateChanged = useCallback(() => {
setJoined(rtcSession.isJoined());
}, [rtcSession]);
useEffect(() => {
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
return () => {
rtcSession.off(
MatrixRTCSessionEvent.JoinStateChanged,
onJoinStateChanged
);
};
}, [rtcSession, onJoinStateChanged]);
return isJoined;
}

View File

@@ -0,0 +1,48 @@
/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
import {
MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useCallback, useEffect, useState } from "react";
export function useMatrixRTCSessionMemberships(
rtcSession: MatrixRTCSession
): CallMembership[] {
const [memberships, setMemberships] = useState(rtcSession.memberships);
const onMembershipsChanged = useCallback(() => {
setMemberships(rtcSession.memberships);
}, [rtcSession]);
useEffect(() => {
rtcSession.on(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged
);
return () => {
rtcSession.off(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged
);
};
}, [rtcSession, onMembershipsChanged]);
return memberships;
}