Merge pull request #1348 from vector-im/dbkr/matrixrtcsession
Switch to the MatrixRTCSession layer in js-sdk
This commit is contained in:
@@ -59,7 +59,7 @@
|
|||||||
"i18next-http-backend": "^1.4.4",
|
"i18next-http-backend": "^1.4.4",
|
||||||
"livekit-client": "^1.12.3",
|
"livekit-client": "^1.12.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6836720e1e1c2cb01d49d6e5fcfc01afc14834ca",
|
||||||
"matrix-widget-api": "^1.3.1",
|
"matrix-widget-api": "^1.3.1",
|
||||||
"mermaid": "^9.0.0",
|
"mermaid": "^9.0.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"{{count, number}}|one": "{{count, number}}",
|
"{{count, number}}|one": "{{count, number}}",
|
||||||
"{{count, number}}|other": "{{count, number}}",
|
"{{count, number}}|other": "{{count, number}}",
|
||||||
"{{count}} stars|one": "{{count}} star",
|
"{{count}} stars|one": "{{count}} stars",
|
||||||
"{{count}} stars|other": "{{count}} stars",
|
"{{count}} stars|other": "{{count}} stars",
|
||||||
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
||||||
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
"Exit full screen": "Exit full screen",
|
"Exit full screen": "Exit full screen",
|
||||||
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
|
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
|
||||||
"Feedback": "Feedback",
|
"Feedback": "Feedback",
|
||||||
"Fetching group call timed out.": "Fetching group call timed out.",
|
|
||||||
"Full screen": "Full screen",
|
"Full screen": "Full screen",
|
||||||
"Go": "Go",
|
"Go": "Go",
|
||||||
"Grid": "Grid",
|
"Grid": "Grid",
|
||||||
@@ -54,7 +53,6 @@
|
|||||||
"How did it go?": "How did it go?",
|
"How did it go?": "How did it go?",
|
||||||
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
||||||
"Include debug logs": "Include debug logs",
|
"Include debug logs": "Include debug logs",
|
||||||
"Incompatible versions": "Incompatible versions",
|
|
||||||
"Inspector": "Inspector",
|
"Inspector": "Inspector",
|
||||||
"Join call": "Join call",
|
"Join call": "Join call",
|
||||||
"Join call now": "Join call now",
|
"Join call now": "Join call now",
|
||||||
@@ -72,7 +70,6 @@
|
|||||||
"Not encrypted": "Not encrypted",
|
"Not encrypted": "Not encrypted",
|
||||||
"Not now, return to home screen": "Not now, return to home screen",
|
"Not now, return to home screen": "Not now, return to home screen",
|
||||||
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Passwords must match": "Passwords must match",
|
"Passwords must match": "Passwords must match",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
@@ -118,7 +115,6 @@
|
|||||||
"Waiting for other participants…": "Waiting for other participants…",
|
"Waiting for other participants…": "Waiting for other participants…",
|
||||||
"Walkie-talkie call": "Walkie-talkie call",
|
"Walkie-talkie call": "Walkie-talkie call",
|
||||||
"Walkie-talkie call name": "Walkie-talkie call name",
|
"Walkie-talkie call name": "Walkie-talkie call name",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
|
|
||||||
"Yes, join call": "Yes, join call",
|
"Yes, join call": "Yes, join call",
|
||||||
"You were disconnected from the call": "You were disconnected from the call",
|
"You were disconnected from the call": "You were disconnected from the call",
|
||||||
"Your feedback": "Your feedback",
|
"Your feedback": "Your feedback",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
23
src/livekit/LivekitFocus.ts
Normal file
23
src/livekit/LivekitFocus.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
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 { Focus } from "matrix-js-sdk/src/matrixrtc/focus";
|
||||||
|
|
||||||
|
export interface LivekitFocus extends Focus {
|
||||||
|
type: "livekit";
|
||||||
|
livekit_service_url: string;
|
||||||
|
livekit_alias: string;
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
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 {
|
|
||||||
ReactNode,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { GroupCall } from "matrix-js-sdk";
|
|
||||||
|
|
||||||
import {
|
|
||||||
OpenIDClientParts,
|
|
||||||
SFUConfig,
|
|
||||||
getSFUConfigWithOpenID,
|
|
||||||
} from "./openIDSFU";
|
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
client: OpenIDClientParts;
|
|
||||||
groupCall: GroupCall;
|
|
||||||
roomName: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SFUConfigContext = createContext<SFUConfig | undefined>(undefined);
|
|
||||||
|
|
||||||
export const useSFUConfig = () => useContext(SFUConfigContext);
|
|
||||||
|
|
||||||
export function OpenIDLoader({ client, groupCall, roomName, children }: Props) {
|
|
||||||
const [state, setState] = useState<
|
|
||||||
SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed
|
|
||||||
>({ kind: "loading" });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const result = await getSFUConfigWithOpenID(
|
|
||||||
client,
|
|
||||||
groupCall,
|
|
||||||
roomName
|
|
||||||
);
|
|
||||||
setState({ kind: "loaded", sfuConfig: result });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to fetch SFU config: ", e);
|
|
||||||
setState({ kind: "failed", error: e as Error });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [client, groupCall, roomName]);
|
|
||||||
|
|
||||||
switch (state.kind) {
|
|
||||||
case "loading":
|
|
||||||
return <LoadingView />;
|
|
||||||
case "failed":
|
|
||||||
return <ErrorView error={state.error} />;
|
|
||||||
case "loaded":
|
|
||||||
return (
|
|
||||||
<SFUConfigContext.Provider value={state.sfuConfig}>
|
|
||||||
{children}
|
|
||||||
</SFUConfigContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SFUConfigLoading = {
|
|
||||||
kind: "loading";
|
|
||||||
};
|
|
||||||
|
|
||||||
type SFUConfigLoaded = {
|
|
||||||
kind: "loaded";
|
|
||||||
sfuConfig: SFUConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SFUConfigFailed = {
|
|
||||||
kind: "failed";
|
|
||||||
error: Error;
|
|
||||||
};
|
|
||||||
@@ -14,82 +14,78 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GroupCall, IOpenIDToken, MatrixClient } from "matrix-js-sdk";
|
import { IOpenIDToken, MatrixClient } from "matrix-js-sdk";
|
||||||
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 { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Config } from "../config/Config";
|
import { LivekitFocus } from "./LivekitFocus";
|
||||||
|
import { useActiveFocus } from "../room/useActiveFocus";
|
||||||
|
|
||||||
export interface SFUConfig {
|
export interface SFUConfig {
|
||||||
url: string;
|
url: string;
|
||||||
jwt: string;
|
jwt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sfuConfigEquals(a?: SFUConfig, b?: SFUConfig): boolean {
|
||||||
|
if (a === undefined && b === undefined) return true;
|
||||||
|
if (a === undefined || b === undefined) return false;
|
||||||
|
|
||||||
|
return a.jwt === b.jwt && a.url === b.url;
|
||||||
|
}
|
||||||
|
|
||||||
// The bits we need from MatrixClient
|
// The bits we need from MatrixClient
|
||||||
export type OpenIDClientParts = Pick<
|
export type OpenIDClientParts = Pick<
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
"getOpenIdToken" | "getDeviceId"
|
"getOpenIdToken" | "getDeviceId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export function useOpenIDSFU(
|
||||||
|
client: OpenIDClientParts,
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
) {
|
||||||
|
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
|
||||||
|
|
||||||
|
const activeFocus = useActiveFocus(rtcSession);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const sfuConfig = activeFocus
|
||||||
|
? await getSFUConfigWithOpenID(client, activeFocus)
|
||||||
|
: undefined;
|
||||||
|
setSFUConfig(sfuConfig);
|
||||||
|
})();
|
||||||
|
}, [client, activeFocus]);
|
||||||
|
|
||||||
|
return sfuConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSFUConfigWithOpenID(
|
export async function getSFUConfigWithOpenID(
|
||||||
client: OpenIDClientParts,
|
client: OpenIDClientParts,
|
||||||
groupCall: GroupCall,
|
activeFocus: LivekitFocus
|
||||||
roomName: string
|
): Promise<SFUConfig | undefined> {
|
||||||
): Promise<SFUConfig> {
|
|
||||||
const openIdToken = await client.getOpenIdToken();
|
const openIdToken = await client.getOpenIdToken();
|
||||||
logger.debug("Got openID token", openIdToken);
|
logger.debug("Got openID token", openIdToken);
|
||||||
|
|
||||||
// if the call has a livekit service URL, try it.
|
|
||||||
if (groupCall.livekitServiceURL) {
|
|
||||||
try {
|
|
||||||
logger.info(
|
|
||||||
`Trying to get JWT from call's configured URL of ${groupCall.livekitServiceURL}...`
|
|
||||||
);
|
|
||||||
const sfuConfig = await getLiveKitJWT(
|
|
||||||
client,
|
|
||||||
groupCall.livekitServiceURL,
|
|
||||||
roomName,
|
|
||||||
openIdToken
|
|
||||||
);
|
|
||||||
logger.info(`Got JWT from call state event URL.`);
|
|
||||||
|
|
||||||
return sfuConfig;
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(
|
|
||||||
`Failed to get JWT from group call's configured URL of ${groupCall.livekitServiceURL}.`,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, try our configured one and, if it works, update the call's service URL in the state event
|
|
||||||
// NB. This wuill update it for everyone so we may end up with multiple clients updating this when they
|
|
||||||
// join at similar times, but we don't have a huge number of options here.
|
|
||||||
const urlFromConf = Config.get().livekit!.livekit_service_url;
|
|
||||||
logger.info(`Trying livekit service URL from our config: ${urlFromConf}...`);
|
|
||||||
try {
|
try {
|
||||||
|
logger.info(
|
||||||
|
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`
|
||||||
|
);
|
||||||
const sfuConfig = await getLiveKitJWT(
|
const sfuConfig = await getLiveKitJWT(
|
||||||
client,
|
client,
|
||||||
urlFromConf,
|
activeFocus.livekit_service_url,
|
||||||
roomName,
|
activeFocus.livekit_alias,
|
||||||
openIdToken
|
openIdToken
|
||||||
);
|
);
|
||||||
|
logger.info(`Got JWT from call's active focus URL.`);
|
||||||
logger.info(
|
|
||||||
`Got JWT, updating call livekit service URL with: ${urlFromConf}...`
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await groupCall.updateLivekitServiceURL(urlFromConf);
|
|
||||||
logger.info(`Call livekit service URL updated.`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(
|
|
||||||
`Failed to update call livekit service URL: continuing anyway.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sfuConfig;
|
return sfuConfig;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to get JWT from URL defined in Config.", e);
|
logger.warn(
|
||||||
throw e;
|
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
src/livekit/useECConnectionState.ts
Normal file
100
src/livekit/useECConnectionState.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
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 { ConnectionState, Room, RoomEvent } from "livekit-client";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { SFUConfig, sfuConfigEquals } from "./openIDSFU";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Additional values for states that a call can be in, beyond what livekit
|
||||||
|
* provides in ConnectionState. Also reconnects the call if the SFU Config
|
||||||
|
* changes.
|
||||||
|
*/
|
||||||
|
export enum ECAddonConnectionState {
|
||||||
|
// We are switching from one focus to another (or between livekit room aliases on the same focus)
|
||||||
|
ECSwitchingFocus = "ec_switching_focus",
|
||||||
|
// The call has just been initialised and is waiting for credentials to arrive before attempting
|
||||||
|
// to connect. This distinguishes from the 'Disconected' state which is now just for when livekit
|
||||||
|
// gives up on connectivity and we consider the call to have failed.
|
||||||
|
ECWaiting = "ec_waiting",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ECConnectionState = ConnectionState | ECAddonConnectionState;
|
||||||
|
|
||||||
|
// This is mostly necessary because an empty useRef is an empty object
|
||||||
|
// which is truthy, so we can't just use Boolean(currentSFUConfig.current)
|
||||||
|
function sfuConfigValid(sfuConfig?: SFUConfig): boolean {
|
||||||
|
return Boolean(sfuConfig?.url) && Boolean(sfuConfig?.jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useECConnectionState(
|
||||||
|
livekitRoom?: Room,
|
||||||
|
sfuConfig?: SFUConfig
|
||||||
|
): ECConnectionState {
|
||||||
|
const [connState, setConnState] = useState(
|
||||||
|
sfuConfig && livekitRoom
|
||||||
|
? livekitRoom.state
|
||||||
|
: ECAddonConnectionState.ECWaiting
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isSwitchingFocus, setSwitchingFocus] = useState(false);
|
||||||
|
|
||||||
|
const onConnStateChanged = useCallback((state: ConnectionState) => {
|
||||||
|
if (state == ConnectionState.Connected) setSwitchingFocus(false);
|
||||||
|
setConnState(state);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const oldRoom = livekitRoom;
|
||||||
|
|
||||||
|
if (livekitRoom) {
|
||||||
|
livekitRoom.on(RoomEvent.ConnectionStateChanged, onConnStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (oldRoom)
|
||||||
|
oldRoom.off(RoomEvent.ConnectionStateChanged, onConnStateChanged);
|
||||||
|
};
|
||||||
|
}, [livekitRoom, onConnStateChanged]);
|
||||||
|
|
||||||
|
const currentSFUConfig = useRef(Object.assign({}, sfuConfig));
|
||||||
|
|
||||||
|
// Id we are transitioning from a valid config to another valid one, we need
|
||||||
|
// to explicitly switch focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
sfuConfigValid(sfuConfig) &&
|
||||||
|
sfuConfigValid(currentSFUConfig.current) &&
|
||||||
|
!sfuConfigEquals(currentSFUConfig.current, sfuConfig)
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`
|
||||||
|
);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
setSwitchingFocus(true);
|
||||||
|
await livekitRoom?.disconnect();
|
||||||
|
await livekitRoom?.connect(sfuConfig!.url, sfuConfig!.jwt);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSFUConfig.current = Object.assign({}, sfuConfig);
|
||||||
|
}, [sfuConfig, livekitRoom]);
|
||||||
|
|
||||||
|
return isSwitchingFocus ? ECAddonConnectionState.ECSwitchingFocus : connState;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RoomOptions,
|
RoomOptions,
|
||||||
setLogLevel,
|
setLogLevel,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { useConnectionState, useLiveKitRoom } from "@livekit/components-react";
|
import { useLiveKitRoom } from "@livekit/components-react";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -35,6 +35,10 @@ import {
|
|||||||
MediaDevices,
|
MediaDevices,
|
||||||
useMediaDevices,
|
useMediaDevices,
|
||||||
} from "./MediaDevicesContext";
|
} from "./MediaDevicesContext";
|
||||||
|
import {
|
||||||
|
ECConnectionState,
|
||||||
|
useECConnectionState,
|
||||||
|
} from "./useECConnectionState";
|
||||||
|
|
||||||
export type E2EEConfig = {
|
export type E2EEConfig = {
|
||||||
sharedKey: string;
|
sharedKey: string;
|
||||||
@@ -42,11 +46,16 @@ export type E2EEConfig = {
|
|||||||
|
|
||||||
setLogLevel("debug");
|
setLogLevel("debug");
|
||||||
|
|
||||||
|
interface UseLivekitResult {
|
||||||
|
livekitRoom?: Room;
|
||||||
|
connState: ECConnectionState;
|
||||||
|
}
|
||||||
|
|
||||||
export function useLiveKit(
|
export function useLiveKit(
|
||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
sfuConfig?: SFUConfig,
|
sfuConfig?: SFUConfig,
|
||||||
e2eeConfig?: E2EEConfig
|
e2eeConfig?: E2EEConfig
|
||||||
): Room | undefined {
|
): UseLivekitResult {
|
||||||
const e2eeOptions = useMemo(() => {
|
const e2eeOptions = useMemo(() => {
|
||||||
if (!e2eeConfig?.sharedKey) return undefined;
|
if (!e2eeConfig?.sharedKey) return undefined;
|
||||||
|
|
||||||
@@ -101,7 +110,7 @@ export function useLiveKit(
|
|||||||
room: roomWithoutProps,
|
room: roomWithoutProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connectionState = useConnectionState(roomWithoutProps);
|
const connectionState = useECConnectionState(room, sfuConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Sync the requested mute states with LiveKit's mute states. We do it this
|
// Sync the requested mute states with LiveKit's mute states. We do it this
|
||||||
@@ -149,5 +158,8 @@ export function useLiveKit(
|
|||||||
}
|
}
|
||||||
}, [room, devices, connectionState]);
|
}, [room, devices, connectionState]);
|
||||||
|
|
||||||
return room;
|
return {
|
||||||
|
connState: connectionState,
|
||||||
|
livekitRoom: room,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ export async function initClient(
|
|||||||
localTimeoutMs: 5000,
|
localTimeoutMs: 5000,
|
||||||
useE2eForGroupCall: e2eEnabled,
|
useE2eForGroupCall: e2eEnabled,
|
||||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||||
useLivekitForGroupCalls: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,29 +16,28 @@ 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 { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
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 { findDeviceByName } from "../media-utils";
|
import { findDeviceByName } from "../media-utils";
|
||||||
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 { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
|
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
||||||
|
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
||||||
import {
|
import {
|
||||||
useManageRoomSharedKey,
|
useManageRoomSharedKey,
|
||||||
useIsRoomE2EE,
|
useIsRoomE2EE,
|
||||||
@@ -52,7 +51,7 @@ import { ShareModal } from "./ShareModal";
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
groupCall?: GroupCall;
|
rtcSession?: MatrixRTCSession;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +61,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({
|
||||||
@@ -71,43 +70,43 @@ export function GroupCallView({
|
|||||||
isEmbedded,
|
isEmbedded,
|
||||||
preload,
|
preload,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
groupCall,
|
rtcSession,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { state, error, enter, leave, participants, otelGroupCallMembership } =
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
useGroupCall(groupCall, client);
|
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
||||||
|
|
||||||
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
|
const e2eeSharedKey = useManageRoomSharedKey(rtcSession.room.roomId);
|
||||||
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
|
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
|
||||||
|
|
||||||
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 roomName = useRoomName(groupCall.room);
|
const roomName = useRoomName(rtcSession.room);
|
||||||
const roomAvatar = useRoomAvatar(groupCall.room);
|
const roomAvatar = useRoomAvatar(rtcSession.room);
|
||||||
const roomEncrypted = useIsRoomE2EE(groupCall.room.roomId)!;
|
const roomEncrypted = useIsRoomE2EE(rtcSession.room.roomId)!;
|
||||||
|
|
||||||
const matrixInfo = useMemo((): MatrixInfo => {
|
const matrixInfo = useMemo((): MatrixInfo => {
|
||||||
return {
|
return {
|
||||||
userId: client.getUserId()!,
|
userId: client.getUserId()!,
|
||||||
displayName: displayName!,
|
displayName: displayName!,
|
||||||
avatarUrl: avatarUrl!,
|
avatarUrl: avatarUrl!,
|
||||||
roomId: groupCall.room.roomId,
|
roomId: rtcSession.room.roomId,
|
||||||
roomName,
|
roomName,
|
||||||
roomAlias: groupCall.room.getCanonicalAlias(),
|
roomAlias: rtcSession.room.getCanonicalAlias(),
|
||||||
roomAvatar,
|
roomAvatar,
|
||||||
roomEncrypted,
|
roomEncrypted,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
groupCall,
|
rtcSession,
|
||||||
roomName,
|
roomName,
|
||||||
roomAvatar,
|
roomAvatar,
|
||||||
roomEncrypted,
|
roomEncrypted,
|
||||||
@@ -116,18 +115,22 @@ export function GroupCallView({
|
|||||||
|
|
||||||
const participatingMembers = useMemo(() => {
|
const participatingMembers = useMemo(() => {
|
||||||
const members: RoomMember[] = [];
|
const members: RoomMember[] = [];
|
||||||
for (const [member, deviceMap] of participants.entries()) {
|
// Count each member only once, regardless of how many devices they use
|
||||||
// Count each member only once, regardless of how many devices they use
|
const addedUserIds = new Set<string>();
|
||||||
if (deviceMap.size > 0) members.push(member);
|
for (const membership of memberships) {
|
||||||
|
if (!addedUserIds.has(membership.member.userId)) {
|
||||||
|
addedUserIds.add(membership.member.userId);
|
||||||
|
members.push(membership.member);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return members;
|
return members;
|
||||||
}, [participants]);
|
}, [memberships]);
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -184,10 +187,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),
|
||||||
@@ -200,19 +206,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);
|
||||||
@@ -223,21 +228,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
|
||||||
@@ -254,13 +254,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);
|
||||||
};
|
};
|
||||||
@@ -269,7 +269,7 @@ export function GroupCallView({
|
|||||||
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [groupCall, state, leave]);
|
}, [isJoined, rtcSession]);
|
||||||
|
|
||||||
const [e2eeEnabled] = useEnableE2EE();
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
|
||||||
@@ -281,10 +281,10 @@ export function GroupCallView({
|
|||||||
const onReconnect = useCallback(() => {
|
const onReconnect = useCallback(() => {
|
||||||
setLeft(false);
|
setLeft(false);
|
||||||
setLeaveError(undefined);
|
setLeaveError(undefined);
|
||||||
groupCall.enter();
|
enterRTCSession(rtcSession);
|
||||||
}, [groupCall]);
|
}, [rtcSession]);
|
||||||
|
|
||||||
const joinRule = useJoinRule(groupCall.room);
|
const joinRule = useJoinRule(rtcSession.room);
|
||||||
|
|
||||||
const { modalState: shareModalState, modalProps: shareModalProps } =
|
const { modalState: shareModalState, modalProps: shareModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
@@ -311,40 +311,27 @@ export function GroupCallView({
|
|||||||
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const livekitServiceURL =
|
|
||||||
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
|
|
||||||
if (!livekitServiceURL) {
|
|
||||||
return <ErrorView error={new Error("No livekit_service_url defined")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shareModal = shareModalState.isOpen && (
|
const shareModal = shareModalState.isOpen && (
|
||||||
<ShareModal roomId={groupCall.room.roomId} {...shareModalProps} />
|
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (isJoined) {
|
||||||
return <ErrorView error={error} />;
|
|
||||||
} else if (state === GroupCallState.Entered) {
|
|
||||||
return (
|
return (
|
||||||
<OpenIDLoader
|
<>
|
||||||
client={client}
|
|
||||||
groupCall={groupCall}
|
|
||||||
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
|
|
||||||
>
|
|
||||||
{shareModal}
|
{shareModal}
|
||||||
<ActiveCall
|
<ActiveCall
|
||||||
client={client}
|
client={client}
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
groupCall={groupCall}
|
rtcSession={rtcSession}
|
||||||
participants={participants}
|
|
||||||
participatingMembers={participatingMembers}
|
participatingMembers={participatingMembers}
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
e2eeConfig={e2eeConfig}
|
e2eeConfig={e2eeConfig}
|
||||||
otelGroupCallMembership={otelGroupCallMembership}
|
//otelGroupCallMembership={otelGroupCallMembership}
|
||||||
onShareClick={onShareClick}
|
onShareClick={onShareClick}
|
||||||
/>
|
/>
|
||||||
</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
|
||||||
@@ -360,7 +347,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}
|
||||||
@@ -389,7 +376,7 @@ export function GroupCallView({
|
|||||||
client={client}
|
client={client}
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
onEnter={() => enter()}
|
onEnter={() => enterRTCSession(rtcSession)}
|
||||||
isEmbedded={isEmbedded}
|
isEmbedded={isEmbedded}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
participatingMembers={participatingMembers}
|
participatingMembers={participatingMembers}
|
||||||
|
|||||||
@@ -23,16 +23,16 @@ import {
|
|||||||
useTracks,
|
useTracks,
|
||||||
} from "@livekit/components-react";
|
} from "@livekit/components-react";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client";
|
import { ConnectionState, Room, 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 { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
|
||||||
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";
|
||||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||||
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 { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import { ReactComponent as LogoMark } from "../icons/LogoMark.svg";
|
import { ReactComponent as LogoMark } from "../icons/LogoMark.svg";
|
||||||
import { ReactComponent as LogoType } from "../icons/LogoType.svg";
|
import { ReactComponent as LogoType } from "../icons/LogoType.svg";
|
||||||
@@ -50,19 +50,14 @@ import {
|
|||||||
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";
|
||||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
||||||
import { ElementWidgetActions, widget } from "../widget";
|
import { ElementWidgetActions, widget } from "../widget";
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
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";
|
||||||
@@ -72,14 +67,14 @@ import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
|||||||
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
||||||
import { useFullscreen } from "./useFullscreen";
|
import { useFullscreen } from "./useFullscreen";
|
||||||
import { useLayoutStates } from "../video-grid/Layout";
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
|
||||||
import { useEventEmitterThree } from "../useEvents";
|
|
||||||
import { useWakeLock } from "../useWakeLock";
|
import { useWakeLock } from "../useWakeLock";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { MatrixInfo } from "./VideoPreview";
|
import { MatrixInfo } from "./VideoPreview";
|
||||||
import { ShareButton } from "../button/ShareButton";
|
import { ShareButton } from "../button/ShareButton";
|
||||||
import { LayoutToggle } from "./LayoutToggle";
|
import { LayoutToggle } from "./LayoutToggle";
|
||||||
|
import { ECConnectionState } from "../livekit/useECConnectionState";
|
||||||
|
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -87,13 +82,18 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
|||||||
// For now we can disable screensharing in Safari.
|
// For now we can disable screensharing in Safari.
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
|
export interface ActiveCallProps
|
||||||
|
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
|
||||||
e2eeConfig?: E2EEConfig;
|
e2eeConfig?: E2EEConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveCall(props: ActiveCallProps) {
|
export function ActiveCall(props: ActiveCallProps) {
|
||||||
const sfuConfig = useSFUConfig();
|
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
||||||
const livekitRoom = useLiveKit(props.muteStates, sfuConfig, props.e2eeConfig);
|
const { livekitRoom, connState } = useLiveKit(
|
||||||
|
props.muteStates,
|
||||||
|
sfuConfig,
|
||||||
|
props.e2eeConfig
|
||||||
|
);
|
||||||
|
|
||||||
if (!livekitRoom) {
|
if (!livekitRoom) {
|
||||||
return null;
|
return null;
|
||||||
@@ -105,7 +105,7 @@ export function ActiveCall(props: ActiveCallProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={livekitRoom}>
|
<RoomContext.Provider value={livekitRoom}>
|
||||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,34 +113,42 @@ export function ActiveCall(props: ActiveCallProps) {
|
|||||||
export interface InCallViewProps {
|
export interface InCallViewProps {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
groupCall: GroupCall;
|
rtcSession: MatrixRTCSession;
|
||||||
livekitRoom: Room;
|
livekitRoom: Room;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
|
||||||
participatingMembers: RoomMember[];
|
participatingMembers: RoomMember[];
|
||||||
onLeave: (error?: Error) => void;
|
onLeave: (error?: Error) => void;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
|
connState: ECConnectionState;
|
||||||
onShareClick: (() => void) | null;
|
onShareClick: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InCallView({
|
export function InCallView({
|
||||||
client,
|
client,
|
||||||
matrixInfo,
|
matrixInfo,
|
||||||
groupCall,
|
rtcSession,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
muteStates,
|
muteStates,
|
||||||
participants,
|
|
||||||
participatingMembers,
|
participatingMembers,
|
||||||
onLeave,
|
onLeave,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
otelGroupCallMembership,
|
otelGroupCallMembership,
|
||||||
|
connState,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}: InCallViewProps) {
|
}: InCallViewProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (connState === ConnectionState.Disconnected) {
|
||||||
|
// annoyingly we don't get the disconnection reason this way,
|
||||||
|
// only by listening for the emitted event
|
||||||
|
onLeave(new Error("Disconnected from call server"));
|
||||||
|
}
|
||||||
|
}, [connState, onLeave]);
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
const boundsValid = bounds.height > 0;
|
const boundsValid = bounds.height > 0;
|
||||||
@@ -157,7 +165,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();
|
||||||
@@ -184,27 +192,10 @@ export function InCallView({
|
|||||||
(muted) => muteStates.audio.setEnabled?.(!muted)
|
(muted) => muteStates.audio.setEnabled?.(!muted)
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDisconnected = useCallback(
|
|
||||||
(reason?: DisconnectReason) => {
|
|
||||||
PosthogAnalytics.instance.eventCallDisconnected.track(reason);
|
|
||||||
logger.info("Disconnected from livekit call with reason ", reason);
|
|
||||||
onLeave(
|
|
||||||
new Error("Disconnected from LiveKit call with reason " + reason)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[onLeave]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onLeavePress = useCallback(() => {
|
const onLeavePress = useCallback(() => {
|
||||||
onLeave();
|
onLeave();
|
||||||
}, [onLeave]);
|
}, [onLeave]);
|
||||||
|
|
||||||
useEventEmitterThree<RoomEvent.Disconnected, RoomEventCallbacks>(
|
|
||||||
livekitRoom,
|
|
||||||
RoomEvent.Disconnected,
|
|
||||||
onDisconnected
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport.send(
|
widget?.api.transport.send(
|
||||||
layout === "grid"
|
layout === "grid"
|
||||||
@@ -245,7 +236,7 @@ export function InCallView({
|
|||||||
const reducedControls = boundsValid && bounds.width <= 340;
|
const reducedControls = boundsValid && bounds.width <= 340;
|
||||||
const noControls = reducedControls && bounds.height <= 400;
|
const noControls = reducedControls && bounds.height <= 400;
|
||||||
|
|
||||||
const items = useParticipantTiles(livekitRoom, participants);
|
const items = useParticipantTiles(livekitRoom, rtcSession.room);
|
||||||
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
||||||
useFullscreen(items);
|
useFullscreen(items);
|
||||||
|
|
||||||
@@ -319,7 +310,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,
|
||||||
@@ -433,24 +424,24 @@ 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -458,25 +449,36 @@ export function InCallView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findMatrixMember(
|
||||||
|
room: MatrixRoom,
|
||||||
|
id: string
|
||||||
|
): RoomMember | undefined {
|
||||||
|
if (!id) return undefined;
|
||||||
|
|
||||||
|
const parts = id.split(":");
|
||||||
|
// must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon
|
||||||
|
if (parts.length < 3) {
|
||||||
|
logger.warn(
|
||||||
|
"Livekit participants ID doesn't look like a userId:deviceId combination"
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.pop();
|
||||||
|
const userId = parts.join(":");
|
||||||
|
|
||||||
|
return room.getMember(userId) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function useParticipantTiles(
|
function useParticipantTiles(
|
||||||
livekitRoom: Room,
|
livekitRoom: Room,
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>
|
matrixRoom: MatrixRoom
|
||||||
): 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(
|
|
||||||
[...participants.entries()].flatMap(([user, devicesMap]) => {
|
|
||||||
return [...devicesMap.keys()].map((deviceId) => [
|
|
||||||
`${user.userId}:${deviceId}`,
|
|
||||||
user,
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPresenter =
|
const hasPresenter =
|
||||||
sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined;
|
sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined;
|
||||||
let allGhosts = true;
|
let allGhosts = true;
|
||||||
@@ -492,7 +494,14 @@ function useParticipantTiles(
|
|||||||
: false;
|
: false;
|
||||||
|
|
||||||
const id = sfuParticipant.identity;
|
const id = sfuParticipant.identity;
|
||||||
const member = matrixParticipants.get(id);
|
const member = findMatrixMember(matrixRoom, id);
|
||||||
|
// We always start with a local participant wit the empty string as their ID before we're
|
||||||
|
// connected, this is fine and we'll be in "all ghosts" mode.
|
||||||
|
if (id !== "" && member === undefined) {
|
||||||
|
logger.warn(
|
||||||
|
`Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!`
|
||||||
|
);
|
||||||
|
}
|
||||||
allGhosts &&= member === undefined;
|
allGhosts &&= member === undefined;
|
||||||
|
|
||||||
const userMediaTile = {
|
const userMediaTile = {
|
||||||
@@ -544,7 +553,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]);
|
}, [matrixRoom, sfuParticipants]);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
68
src/room/useActiveFocus.ts
Normal file
68
src/room/useActiveFocus.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
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";
|
||||||
|
import { deepCompare } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import { LivekitFocus } from "../livekit/LivekitFocus";
|
||||||
|
|
||||||
|
function getActiveFocus(
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
): LivekitFocus | undefined {
|
||||||
|
const oldestMembership = rtcSession.getOldestMembership();
|
||||||
|
return oldestMembership?.getActiveFoci()[0] as LivekitFocus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently active (livekit) focus for a MatrixRTC session
|
||||||
|
* This logic is specific to livekit foci where the whole call must use one
|
||||||
|
* and the same focus.
|
||||||
|
*/
|
||||||
|
export function useActiveFocus(
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
): LivekitFocus | undefined {
|
||||||
|
const [activeFocus, setActiveFocus] = useState(() =>
|
||||||
|
getActiveFocus(rtcSession)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMembershipsChanged = useCallback(() => {
|
||||||
|
const newActiveFocus = getActiveFocus(rtcSession);
|
||||||
|
|
||||||
|
if (!deepCompare(activeFocus, newActiveFocus)) {
|
||||||
|
setActiveFocus(newActiveFocus);
|
||||||
|
}
|
||||||
|
}, [activeFocus, rtcSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rtcSession.on(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rtcSession.off(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return activeFocus;
|
||||||
|
}
|
||||||
@@ -1,629 +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 { useCallback, useEffect, useState } from "react";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import {
|
|
||||||
GroupCallEvent,
|
|
||||||
GroupCallState,
|
|
||||||
GroupCall,
|
|
||||||
GroupCallError,
|
|
||||||
GroupCallStatsReportEvent,
|
|
||||||
GroupCallStatsReport,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
|
||||||
import { MatrixClient, RoomStateEvent } from "matrix-js-sdk";
|
|
||||||
import {
|
|
||||||
ByteSentStatsReport,
|
|
||||||
ConnectionStatsReport,
|
|
||||||
SummaryStatsReport,
|
|
||||||
CallFeedReport,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
|
||||||
|
|
||||||
import { usePageUnload } from "./usePageUnload";
|
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
|
||||||
import { TranslatedError, translatedError } from "../TranslatedError";
|
|
||||||
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
|
||||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
|
||||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
|
||||||
import { checkForParallelCalls } from "./checkForParallelCalls";
|
|
||||||
|
|
||||||
enum ConnectionState {
|
|
||||||
EstablishingCall = "establishing call", // call hasn't been established yet
|
|
||||||
WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
|
|
||||||
Connected = "connected", // media is flowing
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParticipantInfo {
|
|
||||||
connectionState: ConnectionState;
|
|
||||||
presenter: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseGroupCallReturnType {
|
|
||||||
state: GroupCallState;
|
|
||||||
localCallFeed?: CallFeed;
|
|
||||||
activeSpeaker?: CallFeed;
|
|
||||||
userMediaFeeds: CallFeed[];
|
|
||||||
microphoneMuted: boolean;
|
|
||||||
localVideoMuted: boolean;
|
|
||||||
error?: TranslatedError;
|
|
||||||
initLocalCallFeed: () => void;
|
|
||||||
enter: () => Promise<void>;
|
|
||||||
leave: () => void;
|
|
||||||
toggleLocalVideoMuted: () => void;
|
|
||||||
toggleMicrophoneMuted: () => void;
|
|
||||||
toggleScreensharing: () => void;
|
|
||||||
setMicrophoneMuted: (muted: boolean) => void;
|
|
||||||
requestingScreenshare: boolean;
|
|
||||||
isScreensharing: boolean;
|
|
||||||
screenshareFeeds: CallFeed[];
|
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
|
||||||
hasLocalParticipant: boolean;
|
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
state: GroupCallState;
|
|
||||||
localCallFeed?: CallFeed;
|
|
||||||
activeSpeaker?: CallFeed;
|
|
||||||
userMediaFeeds: CallFeed[];
|
|
||||||
error?: TranslatedError;
|
|
||||||
microphoneMuted: boolean;
|
|
||||||
localVideoMuted: boolean;
|
|
||||||
screenshareFeeds: CallFeed[];
|
|
||||||
isScreensharing: boolean;
|
|
||||||
requestingScreenshare: boolean;
|
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
|
||||||
hasLocalParticipant: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a bit of a hack, but we keep the opentelemetry tracker object at the file
|
|
||||||
// level so that it doesn't pop in & out of existence as react mounts & unmounts
|
|
||||||
// components. The right solution is probably for this to live in the js-sdk and have
|
|
||||||
// the same lifetime as groupcalls themselves.
|
|
||||||
let groupCallOTelMembership: OTelGroupCallMembership | undefined;
|
|
||||||
let groupCallOTelMembershipGroupCallId: string;
|
|
||||||
|
|
||||||
function getParticipants(
|
|
||||||
groupCall: GroupCall
|
|
||||||
): Map<RoomMember, Map<string, ParticipantInfo>> {
|
|
||||||
const participants = new Map<RoomMember, Map<string, ParticipantInfo>>();
|
|
||||||
|
|
||||||
for (const [member, participantsStateMap] of groupCall.participants) {
|
|
||||||
const participantInfoMap = new Map<string, ParticipantInfo>();
|
|
||||||
participants.set(member, participantInfoMap);
|
|
||||||
|
|
||||||
for (const [deviceId, participant] of participantsStateMap) {
|
|
||||||
const feed = groupCall.userMediaFeeds.find(
|
|
||||||
(f) => f.userId === member.userId && f.deviceId === deviceId
|
|
||||||
);
|
|
||||||
|
|
||||||
let connectionState: ConnectionState;
|
|
||||||
// If we allow calls without media, we have no feeds and cannot read the connection status from them.
|
|
||||||
// @TODO: The connection state should generally not be determined by the feed.
|
|
||||||
if (
|
|
||||||
groupCall.allowCallWithoutVideoAndAudio &&
|
|
||||||
!feed &&
|
|
||||||
!participant.screensharing
|
|
||||||
) {
|
|
||||||
connectionState = ConnectionState.Connected;
|
|
||||||
} else {
|
|
||||||
connectionState = feed
|
|
||||||
? feed.connected
|
|
||||||
? ConnectionState.Connected
|
|
||||||
: ConnectionState.WaitMedia
|
|
||||||
: ConnectionState.EstablishingCall;
|
|
||||||
}
|
|
||||||
participantInfoMap.set(deviceId, {
|
|
||||||
connectionState,
|
|
||||||
presenter: participant.screensharing,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return participants;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGroupCall(
|
|
||||||
groupCall: GroupCall,
|
|
||||||
client: MatrixClient
|
|
||||||
): UseGroupCallReturnType {
|
|
||||||
const [
|
|
||||||
{
|
|
||||||
state,
|
|
||||||
localCallFeed,
|
|
||||||
activeSpeaker,
|
|
||||||
userMediaFeeds,
|
|
||||||
error,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
isScreensharing,
|
|
||||||
screenshareFeeds,
|
|
||||||
participants,
|
|
||||||
hasLocalParticipant,
|
|
||||||
requestingScreenshare,
|
|
||||||
},
|
|
||||||
setState,
|
|
||||||
] = useState<State>({
|
|
||||||
state: GroupCallState.LocalCallFeedUninitialized,
|
|
||||||
userMediaFeeds: [],
|
|
||||||
microphoneMuted: false,
|
|
||||||
localVideoMuted: false,
|
|
||||||
isScreensharing: false,
|
|
||||||
screenshareFeeds: [],
|
|
||||||
requestingScreenshare: false,
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
hasLocalParticipant: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) {
|
|
||||||
if (groupCallOTelMembership) groupCallOTelMembership.dispose();
|
|
||||||
|
|
||||||
// If the user disables analytics, this will stay around until they leave the call
|
|
||||||
// so analytics will be disabled once they leave.
|
|
||||||
if (ElementCallOpenTelemetry.instance) {
|
|
||||||
groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client);
|
|
||||||
groupCallOTelMembershipGroupCallId = groupCall.groupCallId;
|
|
||||||
} else {
|
|
||||||
groupCallOTelMembership = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateState = useCallback(
|
|
||||||
(state: Partial<State>) => setState((prev) => ({ ...prev, ...state })),
|
|
||||||
[setState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doNothingMediaActionCallback = useCallback(
|
|
||||||
(details: MediaSessionActionDetails) => {},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const leaveCall = useCallback(() => {
|
|
||||||
groupCallOTelMembership?.onLeaveCall();
|
|
||||||
groupCall.leave();
|
|
||||||
}, [groupCall]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// disable the media action keys, otherwise audio elements get paused when
|
|
||||||
// the user presses media keys or unplugs headphones, etc.
|
|
||||||
// Note there are actions for muting / unmuting a microphone & hanging up
|
|
||||||
// which we could wire up.
|
|
||||||
const mediaActions: MediaSessionAction[] = [
|
|
||||||
"play",
|
|
||||||
"pause",
|
|
||||||
"stop",
|
|
||||||
"nexttrack",
|
|
||||||
"previoustrack",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const mediaAction of mediaActions) {
|
|
||||||
navigator.mediaSession?.setActionHandler(
|
|
||||||
mediaAction,
|
|
||||||
doNothingMediaActionCallback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const mediaAction of mediaActions) {
|
|
||||||
navigator.mediaSession?.setActionHandler(mediaAction, null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [doNothingMediaActionCallback]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onGroupCallStateChanged() {
|
|
||||||
updateState({
|
|
||||||
state: groupCall.state,
|
|
||||||
localCallFeed: groupCall.localCallFeed,
|
|
||||||
activeSpeaker: groupCall.activeSpeaker,
|
|
||||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevUserMediaFeeds = new Set<CallFeed>();
|
|
||||||
|
|
||||||
function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
|
|
||||||
for (const feed of prevUserMediaFeeds) {
|
|
||||||
feed.off(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
}
|
|
||||||
prevUserMediaFeeds.clear();
|
|
||||||
|
|
||||||
for (const feed of userMediaFeeds) {
|
|
||||||
feed.on(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
prevUserMediaFeeds.add(feed);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
userMediaFeeds: [...userMediaFeeds],
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevScreenshareFeeds = new Set<CallFeed>();
|
|
||||||
|
|
||||||
function onScreenshareFeedsChanged(screenshareFeeds: CallFeed[]): void {
|
|
||||||
for (const feed of prevScreenshareFeeds) {
|
|
||||||
feed.off(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
}
|
|
||||||
prevScreenshareFeeds.clear();
|
|
||||||
|
|
||||||
for (const feed of screenshareFeeds) {
|
|
||||||
feed.on(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
prevScreenshareFeeds.add(feed);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
screenshareFeeds: [...screenshareFeeds],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onConnectedChanged(connected: boolean): void {
|
|
||||||
updateState({
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
|
|
||||||
updateState({
|
|
||||||
activeSpeaker: activeSpeaker,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocalMuteStateChanged(
|
|
||||||
microphoneMuted: boolean,
|
|
||||||
localVideoMuted: boolean
|
|
||||||
): void {
|
|
||||||
updateState({
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocalScreenshareStateChanged(
|
|
||||||
isScreensharing: boolean,
|
|
||||||
_localScreenshareFeed?: CallFeed,
|
|
||||||
localDesktopCapturerSourceId?: string
|
|
||||||
): void {
|
|
||||||
updateState({
|
|
||||||
isScreensharing,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallsChanged(): void {
|
|
||||||
updateState({ participants: getParticipants(groupCall) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onParticipantsChanged(): void {
|
|
||||||
updateState({
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(e: GroupCallError): void {
|
|
||||||
Sentry.captureException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onConnectionStatsReport(
|
|
||||||
report: GroupCallStatsReport<ConnectionStatsReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onConnectionStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onByteSentStatsReport(
|
|
||||||
report: GroupCallStatsReport<ByteSentStatsReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onByteSentStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSummaryStatsReport(
|
|
||||||
report: GroupCallStatsReport<SummaryStatsReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onSummaryStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallFeedStatsReport(
|
|
||||||
report: GroupCallStatsReport<CallFeedReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onCallFeedStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
|
|
||||||
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallEvent.ScreenshareFeedsChanged,
|
|
||||||
onScreenshareFeedsChanged
|
|
||||||
);
|
|
||||||
groupCall.on(GroupCallEvent.ActiveSpeakerChanged, onActiveSpeakerChanged);
|
|
||||||
groupCall.on(GroupCallEvent.LocalMuteStateChanged, onLocalMuteStateChanged);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallEvent.LocalScreenshareStateChanged,
|
|
||||||
onLocalScreenshareStateChanged
|
|
||||||
);
|
|
||||||
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
|
|
||||||
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
|
|
||||||
groupCall.on(GroupCallEvent.Error, onError);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallStatsReportEvent.ConnectionStats,
|
|
||||||
onConnectionStatsReport
|
|
||||||
);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallStatsReportEvent.ByteSentStats,
|
|
||||||
onByteSentStatsReport
|
|
||||||
);
|
|
||||||
groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallStatsReportEvent.CallFeedStats,
|
|
||||||
onCallFeedStatsReport
|
|
||||||
);
|
|
||||||
|
|
||||||
groupCall.room.currentState.on(
|
|
||||||
RoomStateEvent.Update,
|
|
||||||
checkForParallelCalls
|
|
||||||
);
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
error: undefined,
|
|
||||||
state: groupCall.state,
|
|
||||||
localCallFeed: groupCall.localCallFeed,
|
|
||||||
activeSpeaker: groupCall.activeSpeaker,
|
|
||||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.GroupCallStateChanged,
|
|
||||||
onGroupCallStateChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.UserMediaFeedsChanged,
|
|
||||||
onUserMediaFeedsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.ScreenshareFeedsChanged,
|
|
||||||
onScreenshareFeedsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.ActiveSpeakerChanged,
|
|
||||||
onActiveSpeakerChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.LocalMuteStateChanged,
|
|
||||||
onLocalMuteStateChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.LocalScreenshareStateChanged,
|
|
||||||
onLocalScreenshareStateChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(GroupCallEvent.CallsChanged, onCallsChanged);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.ParticipantsChanged,
|
|
||||||
onParticipantsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.ConnectionStats,
|
|
||||||
onConnectionStatsReport
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.ByteSentStats,
|
|
||||||
onByteSentStatsReport
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.SummaryStats,
|
|
||||||
onSummaryStatsReport
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.CallFeedStats,
|
|
||||||
onCallFeedStatsReport
|
|
||||||
);
|
|
||||||
groupCall.room.currentState.off(
|
|
||||||
RoomStateEvent.Update,
|
|
||||||
checkForParallelCalls
|
|
||||||
);
|
|
||||||
leaveCall();
|
|
||||||
};
|
|
||||||
}, [groupCall, updateState, leaveCall]);
|
|
||||||
|
|
||||||
usePageUnload(() => {
|
|
||||||
leaveCall();
|
|
||||||
});
|
|
||||||
|
|
||||||
const initLocalCallFeed = useCallback(
|
|
||||||
() => groupCall.initLocalCallFeed(),
|
|
||||||
[groupCall]
|
|
||||||
);
|
|
||||||
|
|
||||||
const enter = useCallback(async () => {
|
|
||||||
if (
|
|
||||||
groupCall.state !== GroupCallState.LocalCallFeedUninitialized &&
|
|
||||||
groupCall.state !== GroupCallState.LocalCallFeedInitialized
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
await groupCall.enter().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
updateState({ error });
|
|
||||||
});
|
|
||||||
}, [groupCall, updateState]);
|
|
||||||
|
|
||||||
const toggleLocalVideoMuted = useCallback(() => {
|
|
||||||
const toggleToMute = !groupCall.isLocalVideoMuted();
|
|
||||||
groupCall.setLocalVideoMuted(toggleToMute);
|
|
||||||
groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute);
|
|
||||||
// TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter?
|
|
||||||
PosthogAnalytics.instance.eventMuteCamera.track(
|
|
||||||
toggleToMute,
|
|
||||||
groupCall.groupCallId
|
|
||||||
);
|
|
||||||
}, [groupCall]);
|
|
||||||
|
|
||||||
const setMicrophoneMuted = useCallback(
|
|
||||||
(setMuted: boolean) => {
|
|
||||||
groupCall.setMicrophoneMuted(setMuted);
|
|
||||||
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
|
|
||||||
PosthogAnalytics.instance.eventMuteMicrophone.track(
|
|
||||||
setMuted,
|
|
||||||
groupCall.groupCallId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[groupCall]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleMicrophoneMuted = useCallback(() => {
|
|
||||||
const toggleToMute = !groupCall.isMicrophoneMuted();
|
|
||||||
groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute);
|
|
||||||
setMicrophoneMuted(toggleToMute);
|
|
||||||
}, [groupCall, setMicrophoneMuted]);
|
|
||||||
|
|
||||||
const toggleScreensharing = useCallback(async () => {
|
|
||||||
groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing);
|
|
||||||
|
|
||||||
if (!groupCall.isScreensharing()) {
|
|
||||||
// toggling on
|
|
||||||
updateState({ requestingScreenshare: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await groupCall.setScreensharingEnabled(true, {
|
|
||||||
audio: true,
|
|
||||||
throwOnFail: true,
|
|
||||||
});
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
} catch (e) {
|
|
||||||
// this will fail in Electron because getDisplayMedia just throws a permission
|
|
||||||
// error, so if we have a widget API, try requesting via that.
|
|
||||||
if (widget) {
|
|
||||||
const reply = await widget.api.transport.send(
|
|
||||||
ElementWidgetActions.ScreenshareRequest,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
if (!reply.pending) {
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// toggling off
|
|
||||||
groupCall.setScreensharingEnabled(false);
|
|
||||||
}
|
|
||||||
}, [groupCall, updateState]);
|
|
||||||
|
|
||||||
const onScreenshareStart = useCallback(
|
|
||||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
|
|
||||||
const data = ev.detail.data as unknown as ScreenshareStartData;
|
|
||||||
|
|
||||||
await groupCall.setScreensharingEnabled(true, {
|
|
||||||
desktopCapturerSourceId: data.desktopCapturerSourceId as string,
|
|
||||||
audio: !data.desktopCapturerSourceId,
|
|
||||||
});
|
|
||||||
await widget?.api.transport.reply(ev.detail, {});
|
|
||||||
},
|
|
||||||
[groupCall, updateState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onScreenshareStop = useCallback(
|
|
||||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
await groupCall.setScreensharingEnabled(false);
|
|
||||||
await widget?.api.transport.reply(ev.detail, {});
|
|
||||||
},
|
|
||||||
[groupCall, updateState]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (widget) {
|
|
||||||
widget.lazyActions.on(
|
|
||||||
ElementWidgetActions.ScreenshareStart,
|
|
||||||
onScreenshareStart
|
|
||||||
);
|
|
||||||
widget.lazyActions.on(
|
|
||||||
ElementWidgetActions.ScreenshareStop,
|
|
||||||
onScreenshareStop
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
widget?.lazyActions.off(
|
|
||||||
ElementWidgetActions.ScreenshareStart,
|
|
||||||
onScreenshareStart
|
|
||||||
);
|
|
||||||
widget?.lazyActions.off(
|
|
||||||
ElementWidgetActions.ScreenshareStop,
|
|
||||||
onScreenshareStop
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [onScreenshareStart, onScreenshareStop]);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (window.RTCPeerConnection === undefined) {
|
|
||||||
const error = translatedError(
|
|
||||||
"WebRTC is not supported or is being blocked in this browser.",
|
|
||||||
t
|
|
||||||
);
|
|
||||||
console.error(error);
|
|
||||||
updateState({ error });
|
|
||||||
}
|
|
||||||
}, [t, updateState]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
localCallFeed,
|
|
||||||
activeSpeaker,
|
|
||||||
userMediaFeeds,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
error,
|
|
||||||
initLocalCallFeed,
|
|
||||||
enter,
|
|
||||||
leave: leaveCall,
|
|
||||||
toggleLocalVideoMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
toggleScreensharing,
|
|
||||||
setMicrophoneMuted,
|
|
||||||
requestingScreenshare,
|
|
||||||
isScreensharing,
|
|
||||||
screenshareFeeds,
|
|
||||||
participants,
|
|
||||||
hasLocalParticipant,
|
|
||||||
otelGroupCallMembership: groupCallOTelMembership,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -15,32 +15,23 @@ 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 { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
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 { setLocalStorageItem } from "../useLocalStorage";
|
import { setLocalStorageItem } from "../useLocalStorage";
|
||||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
import { translatedError } from "../TranslatedError";
|
|
||||||
import { widget } from "../widget";
|
|
||||||
import { useEnableE2EE } from "../settings/useSetting";
|
import { useEnableE2EE } from "../settings/useSetting";
|
||||||
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
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 = {
|
||||||
@@ -130,61 +121,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 () => {
|
||||||
@@ -207,7 +149,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, e2eeEnabled]);
|
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
|
||||||
}
|
|
||||||
53
src/rtcSessionHelpers.ts
Normal file
53
src/rtcSessionHelpers.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
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 } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
|
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||||
|
import { LivekitFocus } from "./livekit/LivekitFocus";
|
||||||
|
import { Config } from "./config/Config";
|
||||||
|
|
||||||
|
function makeFocus(livekitAlias: string): LivekitFocus {
|
||||||
|
const urlFromConf = Config.get().livekit!.livekit_service_url;
|
||||||
|
if (!urlFromConf) {
|
||||||
|
throw new Error("No livekit_service_url is configured!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "livekit",
|
||||||
|
livekit_service_url: urlFromConf,
|
||||||
|
livekit_alias: livekitAlias,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enterRTCSession(rtcSession: MatrixRTCSession) {
|
||||||
|
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 asume everything is a room-scoped call
|
||||||
|
const livekitAlias = rtcSession.room.roomId;
|
||||||
|
|
||||||
|
rtcSession.joinRoomSession([makeFocus(livekitAlias)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function leaveRTCSession(rtcSession: MatrixRTCSession) {
|
||||||
|
//groupCallOTelMembership?.onLeaveCall();
|
||||||
|
rtcSession.leaveRoomSession();
|
||||||
|
}
|
||||||
50
src/useMatrixRTCSessionJoinState.ts
Normal file
50
src/useMatrixRTCSessionJoinState.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
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(() => {
|
||||||
|
logger.info(
|
||||||
|
`Session in room ${rtcSession.room.roomId} changed to ${
|
||||||
|
rtcSession.isJoined() ? "joined" : "left"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
setJoined(rtcSession.isJoined());
|
||||||
|
}, [rtcSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rtcSession.off(
|
||||||
|
MatrixRTCSessionEvent.JoinStateChanged,
|
||||||
|
onJoinStateChanged
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [rtcSession, onJoinStateChanged]);
|
||||||
|
|
||||||
|
return isJoined;
|
||||||
|
}
|
||||||
52
src/useMatrixRTCSessionMemberships.ts
Normal file
52
src/useMatrixRTCSessionMemberships.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
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(() => {
|
||||||
|
logger.info(
|
||||||
|
`Memberships changed for call in room ${rtcSession.room.roomId} (${rtcSession.memberships.length} members)`
|
||||||
|
);
|
||||||
|
setMemberships(rtcSession.memberships);
|
||||||
|
}, [rtcSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rtcSession.on(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rtcSession.off(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [rtcSession, onMembershipsChanged]);
|
||||||
|
|
||||||
|
return memberships;
|
||||||
|
}
|
||||||
@@ -157,15 +157,6 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
useE2eForGroupCall: e2eEnabled,
|
useE2eForGroupCall: e2eEnabled,
|
||||||
fallbackICEServerAllowed: allowIceFallback,
|
fallbackICEServerAllowed: allowIceFallback,
|
||||||
// XXX: The client expects the list of foci in its constructor, but we don't
|
|
||||||
// know this until we fetch the config file. However, we can't wait to construct
|
|
||||||
// the client object or we'll miss the 'capabilities' request from the host app.
|
|
||||||
// As of writing this, I have made the embedded widget client send the 'contentLoaded'
|
|
||||||
// message so that we can use the widget API in less racy mode, but we need to change
|
|
||||||
// element-web to use waitForIFrameLoad=false. Once that change has rolled out,
|
|
||||||
// we can just start the client after we've fetched the config.
|
|
||||||
livekitServiceURL: undefined,
|
|
||||||
useLivekitForGroupCalls: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,13 +165,6 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
// wait for the config file to be ready (we load very early on so it might not
|
// wait for the config file to be ready (we load very early on so it might not
|
||||||
// be otherwise)
|
// be otherwise)
|
||||||
await Config.init();
|
await Config.init();
|
||||||
const livekit = Config.get().livekit;
|
|
||||||
const focus = livekit?.livekit_service_url;
|
|
||||||
// Now we've fetched the config, be evil and use the getter to inject the focus
|
|
||||||
// into the client (see above XXX).
|
|
||||||
if (focus) {
|
|
||||||
client.setLivekitServiceURL(livekit.livekit_service_url);
|
|
||||||
}
|
|
||||||
await client.startClient();
|
await client.startClient();
|
||||||
resolve(client);
|
resolve(client);
|
||||||
})();
|
})();
|
||||||
|
|||||||
26
yarn.lock
26
yarn.lock
@@ -2288,10 +2288,10 @@
|
|||||||
clsx "^2.0.0"
|
clsx "^2.0.0"
|
||||||
usehooks-ts "^2.9.1"
|
usehooks-ts "^2.9.1"
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.1":
|
"@matrix-org/matrix-sdk-crypto-wasm@^1.2.3-alpha.0":
|
||||||
version "0.1.4"
|
version "1.2.3-alpha.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.4.tgz#c13c7c8c3a1d8da08e6ad195d25e5e61cc402df7"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-1.2.3-alpha.0.tgz#f6f93e3ee44c5f1e0e255badd26f4a7d3fb1dab8"
|
||||||
integrity sha512-OxG84iSeR89zYLFeb+DCaFtZT+DDiIu+kTkqY8OYfhE5vpGLFX2sDVBRrAdos1IUqEoboDloDBR9+yU7hNRyog==
|
integrity sha512-BFLqfq/WbYZ+83r4UWLhwtBYvTp5DKTHNeWUSDBVvudFtqBvkntNAAUz+xmhmO1XkyNm+sBaElxF8IS9S8zdww==
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||||
version "3.2.14"
|
version "3.2.14"
|
||||||
@@ -11624,29 +11624,29 @@ matrix-events-sdk@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
||||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#6836720e1e1c2cb01d49d6e5fcfc01afc14834ca":
|
||||||
version "26.2.0"
|
version "28.0.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b698217445318f453e0b1086364a33113eaa85d9"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6836720e1e1c2cb01d49d6e5fcfc01afc14834ca"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@matrix-org/matrix-sdk-crypto-js" "^0.1.1"
|
"@matrix-org/matrix-sdk-crypto-wasm" "^1.2.3-alpha.0"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
bs58 "^5.0.0"
|
bs58 "^5.0.0"
|
||||||
content-type "^1.0.4"
|
content-type "^1.0.4"
|
||||||
jwt-decode "^3.1.2"
|
jwt-decode "^3.1.2"
|
||||||
loglevel "^1.7.1"
|
loglevel "^1.7.1"
|
||||||
matrix-events-sdk "0.0.1"
|
matrix-events-sdk "0.0.1"
|
||||||
matrix-widget-api "^1.3.1"
|
matrix-widget-api "^1.6.0"
|
||||||
oidc-client-ts "^2.2.4"
|
oidc-client-ts "^2.2.4"
|
||||||
p-retry "4"
|
p-retry "4"
|
||||||
sdp-transform "^2.14.1"
|
sdp-transform "^2.14.1"
|
||||||
unhomoglyph "^1.0.6"
|
unhomoglyph "^1.0.6"
|
||||||
uuid "9"
|
uuid "9"
|
||||||
|
|
||||||
matrix-widget-api@^1.3.1:
|
matrix-widget-api@^1.3.1, matrix-widget-api@^1.6.0:
|
||||||
version "1.4.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz#e426ec16a013897f3a4a9c2bff423f54ab0ba745"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.6.0.tgz#f0075411edffc6de339580ade7e6e6e6edb01af4"
|
||||||
integrity sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A==
|
integrity sha512-VXIJyAZ/WnBmT4C7ePqevgMYGneKMCP/0JuCOqntSsaNlCRHJvwvTxmqUU+ufOpzIF5gYNyIrAjbgrEbK3iqJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user