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 { HTMLAttributes, ReactNode, useCallback } from "react";
import { HTMLAttributes, ReactNode } from "react";
import { Link } from "react-router-dom";
import { Room } from "matrix-js-sdk/src/models/room";
import { useTranslation } from "react-i18next";
import styles from "./Header.module.css";
import { useModalTriggerState } from "./Modal";
import { Button } from "./button";
import { ReactComponent as Logo } from "./icons/Logo.svg";
import { Subtitle } from "./typography/Typography";
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
interface HeaderProps extends HTMLAttributes<HTMLElement> {
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,
useE2eForGroupCall: e2eEnabled,
fallbackICEServerAllowed: fallbackICEServerAllowed,
useLivekitForGroupCalls: true,
});
try {

View File

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

View File

@@ -16,33 +16,35 @@ limitations under the License.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
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 { useTranslation } from "react-i18next";
import { Room } from "livekit-client";
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 { widget, ElementWidgetActions, JoinCallData } from "../widget";
import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
import { MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { E2EEConfig } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
//import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
import { Config } from "../config/Config";
import { MuteStates, useMuteStates } from "./MuteStates";
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 {
interface Window {
groupCall?: GroupCall;
rtcSession?: MatrixRTCSession;
}
}
@@ -52,7 +54,7 @@ interface Props {
isEmbedded: boolean;
preload: boolean;
hideHeader: boolean;
groupCall: GroupCall;
rtcSession: MatrixRTCSession;
}
export function GroupCallView({
@@ -61,9 +63,9 @@ export function GroupCallView({
isEmbedded,
preload,
hideHeader,
groupCall,
rtcSession,
}: Props) {
const {
/*const {
state,
error,
enter,
@@ -71,33 +73,36 @@ export function GroupCallView({
participants,
unencryptedEventsFromUsers,
otelGroupCallMembership,
} = useGroupCall(groupCall, client);
} = useGroupCall(groupCall, client);*/
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const { t } = useTranslation();
useEffect(() => {
window.groupCall = groupCall;
window.rtcSession = rtcSession;
return () => {
delete window.groupCall;
delete window.rtcSession;
};
}, [groupCall]);
}, [rtcSession]);
const { displayName, avatarUrl } = useProfile(client);
const matrixInfo = useMemo((): MatrixInfo => {
return {
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: groupCall.room.roomId,
roomName: groupCall.room.name,
roomAlias: groupCall.room.getCanonicalAlias(),
roomId: rtcSession.room.roomId,
roomName: rtcSession.room.name,
roomAlias: rtcSession.room.getCanonicalAlias(),
};
}, [displayName, avatarUrl, groupCall]);
}, [displayName, avatarUrl, rtcSession]);
const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices>();
latestDevices.current = deviceContext;
const muteStates = useMuteStates(participants.size);
const muteStates = useMuteStates(memberships.length);
const latestMuteStates = useRef<MuteStates>();
latestMuteStates.current = muteStates;
@@ -154,10 +159,13 @@ export function GroupCallView({
}
}
await enter();
enterRTCSession(rtcSession);
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([
widget!.api.setAlwaysOnScreen(true),
@@ -170,19 +178,18 @@ export function GroupCallView({
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
}
}, [groupCall, preload, enter]);
}, [rtcSession, preload]);
useEffect(() => {
if (isEmbedded && !preload) {
// In embedded mode, bypass the lobby and just enter the call straight away
enter();
enterRTCSession(rtcSession);
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]);
useSentryGroupCallHandler(groupCall);
}, [rtcSession, isEmbedded, preload]);
const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
@@ -193,21 +200,16 @@ export function GroupCallView({
setLeaveError(leaveError);
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,
// therefore we want the event to be sent instantly without getting queued/batched.
const sendInstantly = !!widget;
PosthogAnalytics.instance.eventCallEnded.track(
groupCall.groupCallId,
participantCount,
rtcSession.room.roomId,
rtcSession.memberships.length,
sendInstantly
);
leave();
leaveRTCSession(rtcSession);
if (widget) {
// 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
@@ -224,13 +226,13 @@ export function GroupCallView({
history.push("/");
}
},
[groupCall, leave, isPasswordlessUser, isEmbedded, history]
[rtcSession, isPasswordlessUser, isEmbedded, history]
);
useEffect(() => {
if (widget && state === GroupCallState.Entered) {
if (widget && isJoined) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leave();
leaveRTCSession(rtcSession);
await widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false);
};
@@ -239,7 +241,7 @@ export function GroupCallView({
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
};
}
}, [groupCall, state, leave]);
}, [isJoined, rtcSession]);
const [e2eeConfig, setE2EEConfig] = useState<E2EEConfig | undefined>(
undefined
@@ -248,36 +250,40 @@ export function GroupCallView({
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
groupCall.enter();
}, [groupCall]);
rtcSession.joinRoomSession();
}, [rtcSession]);
const livekitServiceURL =
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
if (!livekitServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
const focus: Focus | undefined = rtcSession
.getOldestMembership()
?.getActiveFoci()?.[0];
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) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
if (isJoined) {
return (
<OpenIDLoader
/*<OpenIDLoader
client={client}
groupCall={groupCall}
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
>
<ActiveCall
client={client}
groupCall={groupCall}
participants={participants}
onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
muteStates={muteStates}
e2eeConfig={e2eeConfig}
otelGroupCallMembership={otelGroupCallMembership}
/>
</OpenIDLoader>
>*/
<ActiveCall
client={client}
rtcSession={rtcSession}
memberships={memberships}
onLeave={onLeave}
hideHeader={hideHeader}
muteStates={muteStates}
e2eeConfig={e2eeConfig}
//otelGroupCallMembership={otelGroupCallMembership}
/>
//</OpenIDLoader>
);
} else if (left) {
// The call ended view is shown for two reasons: prompting guests to create
@@ -293,7 +299,7 @@ export function GroupCallView({
) {
return (
<CallEndedView
endedCallId={groupCall.groupCallId}
endedCallId={rtcSession.room.roomId}
client={client}
isPasswordlessUser={isPasswordlessUser}
leaveError={leaveError}
@@ -321,7 +327,7 @@ export function GroupCallView({
muteStates={muteStates}
onEnter={(e2eeConfig?: E2EEConfig) => {
setE2EEConfig(e2eeConfig);
enter();
enterRTCSession(rtcSession);
}}
isEmbedded={isEmbedded}
hideHeader={hideHeader}

View File

@@ -27,7 +27,6 @@ import classNames from "classnames";
import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
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 { useTranslation } from "react-i18next";
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 { logger } from "matrix-js-sdk/src/logger";
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 {
@@ -45,22 +46,13 @@ import {
SettingsButton,
InviteButton,
} from "../button";
import {
Header,
LeftNav,
RightNav,
RoomHeaderInfo,
VersionMismatchWarning,
} from "../Header";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import {
useVideoGridLayout,
TileDescriptor,
VideoGrid,
} from "../video-grid/VideoGrid";
import {
useShowInspector,
useShowConnectionStats,
} from "../settings/useSetting";
import { useShowConnectionStats } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams";
@@ -68,10 +60,8 @@ import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ElementWidgetActions, widget } from "../widget";
import { GridLayoutMenu } from "./GridLayoutMenu";
import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css";
import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
@@ -120,24 +110,22 @@ export function ActiveCall(props: ActiveCallProps) {
export interface InCallViewProps {
client: MatrixClient;
groupCall: GroupCall;
rtcSession: MatrixRTCSession;
livekitRoom: Room;
muteStates: MuteStates;
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
memberships: CallMembership[];
onLeave: (error?: Error) => void;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
}
export function InCallView({
client,
groupCall,
rtcSession,
livekitRoom,
muteStates,
participants,
memberships,
onLeave,
unencryptedEventsFromUsers,
hideHeader,
otelGroupCallMembership,
}: InCallViewProps) {
@@ -161,7 +149,7 @@ export function InCallView({
screenSharingTracks.length > 0
);
const [showInspector] = useShowInspector();
//const [showInspector] = useShowInspector();
const [showConnectionStats] = useShowConnectionStats();
const { hideScreensharing } = useUrlParams();
@@ -179,7 +167,7 @@ export function InCallView({
[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.
// 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 noControls = reducedControls && bounds.height <= 400;
const items = useParticipantTiles(livekitRoom, participants);
const items = useParticipantTiles(livekitRoom, memberships);
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
useFullscreen(items);
@@ -324,7 +312,7 @@ export function InCallView({
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
} = useRageshakeRequestModal(rtcSession.room.roomId);
const {
modalState: settingsModalState,
@@ -419,11 +407,7 @@ export function InCallView({
{!hideHeader && maximisedParticipant === null && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={groupCall.room.name} />
<VersionMismatchWarning
users={unencryptedEventsFromUsers}
room={groupCall.room}
/>
<RoomHeaderInfo roomName={rtcSession.room.name} />
<E2EELock />
</LeftNav>
<RightNav>
@@ -439,31 +423,31 @@ export function InCallView({
{renderContent()}
{footer}
</div>
{otelGroupCallMembership && (
{/*otelGroupCallMembership && (
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
)}
)*/}
{rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomId={groupCall.room.roomId}
roomId={rtcSession.room.roomId}
/>
)}
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
roomId={rtcSession.room.roomId}
{...settingsModalProps}
/>
)}
{inviteModalState.isOpen && (
<InviteModal
roomIdOrAlias={
groupCall.room.getCanonicalAlias() ?? groupCall.room.roomId
rtcSession.room.getCanonicalAlias() ?? rtcSession.room.roomId
}
{...inviteModalProps}
/>
@@ -474,22 +458,26 @@ export function InCallView({
function useParticipantTiles(
livekitRoom: Room,
participants: Map<RoomMember, Map<string, ParticipantInfo>>
memberships: CallMembership[]
): TileDescriptor<ItemData>[] {
const sfuParticipants = useParticipants({
room: livekitRoom,
});
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(
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]) => {
return [...devicesMap.keys()].map((deviceId) => [
`${user.userId}:${deviceId}`,
user,
]);
})
);
);*/
const hasPresenter =
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
// shouldn't bother showing anything yet
return allGhosts ? [] : tiles;
}, [participants, sfuParticipants]);
}, [memberships, sfuParticipants]);
return items;
}

View File

@@ -15,8 +15,8 @@ limitations under the License.
*/
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 { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
@@ -73,10 +73,10 @@ export const RoomPage: FC = () => {
]);
const groupCallView = useCallback(
(groupCall: GroupCall) => (
(rtcSession: MatrixRTCSession) => (
<GroupCallView
client={client!}
groupCall={groupCall}
rtcSession={rtcSession}
isPasswordlessUser={passwordlessUser}
isEmbedded={isEmbedded}
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");
you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import {
SummaryStatsReport,
CallFeedReport,
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { usePageUnload } from "./usePageUnload";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
@@ -77,7 +78,6 @@ interface UseGroupCallReturnType {
screenshareFeeds: CallFeed[];
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
hasLocalParticipant: boolean;
unencryptedEventsFromUsers: Set<string>;
otelGroupCallMembership?: OTelGroupCallMembership;
}
@@ -103,14 +103,14 @@ interface State {
let groupCallOTelMembership: OTelGroupCallMembership | undefined;
let groupCallOTelMembershipGroupCallId: string;
function getParticipants(
groupCall: GroupCall
/*function getParticipants(
rtcSession: MatrixRTCSession
): 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>();
participants.set(member, participantInfoMap);
participants.set(membership.member, participantInfoMap);
for (const [deviceId, participant] of participantsStateMap) {
const feed = groupCall.userMediaFeeds.find(
@@ -141,10 +141,10 @@ function getParticipants(
}
return participants;
}
}*/
export function useGroupCall(
groupCall: GroupCall,
rtcSession: MatrixRTCSession,
client: MatrixClient
): UseGroupCallReturnType {
const [
@@ -171,7 +171,7 @@ export function useGroupCall(
isScreensharing: false,
screenshareFeeds: [],
requestingScreenshare: false,
participants: getParticipants(groupCall),
participants: getParticipants(rtcSession),
hasLocalParticipant: false,
});

View File

@@ -15,28 +15,19 @@ limitations under the License.
*/
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 { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { SyncState } from "matrix-js-sdk/src/sync";
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 { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
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 = {
kind: "loaded";
groupCall: GroupCall;
rtcSession: MatrixRTCSession;
};
export type GroupCallLoadFailed = {
@@ -115,61 +106,12 @@ export const useLoadGroupCall = (
}
};
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
const room = await fetchOrCreateRoom();
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
let groupCall = client.getGroupCallForRoom(room.roomId);
logger.debug("Got group call", groupCall?.groupCallId);
if (groupCall) {
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
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 rtcSession = client.matrixRTC.getRoomSession(room);
return rtcSession;
};
const waitForClientSyncing = async () => {
@@ -192,7 +134,7 @@ export const useLoadGroupCall = (
waitForClientSyncing()
.then(fetchOrCreateGroupCall)
.then((groupCall) => setState({ kind: "loaded", groupCall }))
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
.catch((error) => setState({ kind: "failed", error }));
}, [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;
}