Compare commits

...

69 Commits

Author SHA1 Message Date
Šimon Brandner
28035b5028 Post-merge fix
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-12 18:34:36 +02:00
Šimon Brandner
d14d1e10c5 Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/e2ee-embeded
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-12 18:30:34 +02:00
David Baker
9efb15941c Merge pull request #1348 from vector-im/dbkr/matrixrtcsession
Switch to the MatrixRTCSession layer in js-sdk
2023-09-12 16:33:02 +01:00
David Baker
a4d20f85a0 Use js-sdk from latest develop 2023-09-12 16:25:59 +01:00
David Baker
59f719acb6 Merge pull request #1420 from vector-im/dbkr/apple-site-association
Add apple site association
2023-09-12 14:00:13 +01:00
David Baker
054361ab3a Merge pull request #1431 from vector-im/renovate/livekit-components-react-1.x-lockfile
Update dependency @livekit/components-react to v1.1.8
2023-09-12 13:59:29 +01:00
renovate[bot]
8f76750062 Update dependency @livekit/components-react to v1.1.8 2023-09-12 12:55:08 +00:00
David Baker
1fb210c5cc Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-09-12 11:30:46 +01:00
David Baker
9d9765f668 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-09-11 09:58:03 +01:00
Šimon Brandner
fce220c55f Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-10 12:11:03 +02:00
Šimon Brandner
645b123bea Handle indices
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-10 12:10:13 +02:00
Šimon Brandner
a0f1184c40 Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 18:29:30 +02:00
Šimon Brandner
ff9982683e Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 18:07:43 +02:00
Šimon Brandner
2f6b1ea37c Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 17:45:42 +02:00
Šimon Brandner
3a62eccdea Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 17:39:33 +02:00
Šimon Brandner
8bae276fab Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 17:32:35 +02:00
Šimon Brandner
3d57faca26 Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 17:14:59 +02:00
Šimon Brandner
3a16dbe54e Update encryption key on mute change
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 17:14:01 +02:00
Šimon Brandner
ba5c0424bf Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-07 17:02:01 +02:00
David Baker
7b14d9dacb Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-09-07 14:09:52 +01:00
David Baker
da5bef1206 Wrong finger or something 2023-09-06 16:23:49 +01:00
David Baker
b0e7b949da Assetlinks for android too 2023-09-06 16:03:54 +01:00
David Baker
1757bc546d Rename to not have file extension
As this is what apple need. Also make the docker container serve
it with the right content type.
2023-09-06 13:59:37 +01:00
David Baker
5d969563f7 Prettier 2023-09-06 10:35:41 +01:00
David Baker
ef037c5c1d Add apple site association 2023-09-06 10:22:02 +01:00
David Baker
8f570b7893 Include the colon in the user ID in the numebr of parts we check for 2023-09-06 09:12:04 +01:00
David Baker
83fd9a49ab Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-09-06 09:01:58 +01:00
Šimon Brandner
6bc6d50bce Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-06 08:27:33 +02:00
Šimon Brandner
0ac651b03a Explicit logging
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-06 08:18:37 +02:00
Šimon Brandner
18185be49c Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 18:23:22 +02:00
Šimon Brandner
8516e528cb Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 17:02:33 +02:00
Šimon Brandner
5862f911bc Fix map
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 16:56:08 +02:00
Šimon Brandner
2ec75571d0 Fix useEnableSPAE2EE
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 16:15:54 +02:00
Šimon Brandner
720e138199 Merge remote-tracking branch 'upstream/dbkr/matrixrtcsession' into SimonBrandner/feat/e2ee-embeded
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 16:10:15 +02:00
Šimon Brandner
ae04cfc53e Add a comment
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 16:04:49 +02:00
Šimon Brandner
912ed55f1e Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-09-04 16:01:32 +02:00
David Baker
0dbd3c2a58 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-09-01 15:09:24 +01:00
David Baker
fe4d42f4d0 Update js-sdk 2023-08-31 14:43:53 +01:00
David Baker
981a29ebf7 yarn 2023-08-31 14:38:31 +01:00
David Baker
dd9397d921 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-08-31 14:38:05 +01:00
David Baker
0535a35a1c Remove unused memberships prop 2023-08-31 14:36:54 +01:00
Šimon Brandner
1a7b7c9470 Update-js sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-31 14:13:02 +02:00
David Baker
9d6d5a16e7 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-08-31 10:48:54 +01:00
David Baker
b256755a0d Don't treat empty object as a valid sfu config
This was causing an extra reconnect cycle when the call was first
joined because it thought the previous SFU config was valid. This was
probably causing some client to fail to connect at all.
2023-08-31 09:44:23 +01:00
David Baker
b5626dd97c Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-08-30 17:25:00 +01:00
Šimon Brandner
ee304fb4b3 Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 12:46:39 +02:00
Šimon Brandner
b57454fbc1 Change embedded E2EE implementation
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 11:42:01 +02:00
Šimon Brandner
127e8c9103 sharedKeyManagement.ts -> e2eeHooks.ts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 11:37:38 +02:00
Šimon Brandner
f279bd69d7 Remove unnecessary call
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 11:37:38 +02:00
Šimon Brandner
b45c8924a6 Handle embedding
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 11:37:37 +02:00
Šimon Brandner
fa5b014abe E2EE for embeded mode
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-30 11:36:42 +02:00
David Baker
923f081d74 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-08-30 10:04:43 +01:00
David Baker
192c3295c9 Update js-sdk 2023-08-29 14:19:08 +01:00
David Baker
4cd274b91e Manually disconnect & reconnect the livekit call if our focus changes
Without breaking the 'disconnected' screen
2023-08-29 12:45:34 +01:00
Šimon Brandner
992e6aa2a3 Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-08-28 17:01:58 +02:00
David Baker
22533fc408 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-08-25 14:40:52 +01:00
David Baker
e4e35eecfd Use Matrix room membership to look up room member
Rather than the matrixRTC memberships. We're essentially trusting
LiveKit's view of weho is connected here, so we may as well include
the real names of anyone we don't think is a matrixRTC participant,
for whatever reason.
2023-08-25 13:19:23 +01:00
David Baker
dc80a7e350 Log ghosts
and remove unsused code
2023-08-23 15:04:41 +01:00
David Baker
1955765cfd Bump js-sdk 2023-08-23 14:03:38 +01:00
David Baker
af5e679089 i18n 2023-08-23 10:15:29 +01:00
David Baker
c516207199 Remove matrix-widget-api as a dep & remove livekit stuff from widget init
We'll always have matrix-widget-api as a dep through js-sdk so also
specifyin it ourselves just means we'll end up using a different version
when the js-sdk upgrade their copy and get wierd errors. We could add a
peerDependency if we really felt the need?
2023-08-23 10:12:09 +01:00
David Baker
918736e758 Removed now unused useGroupCall & OpenIDLoader 2023-08-23 10:06:08 +01:00
David Baker
095753c6a0 i18n 2023-08-23 10:03:05 +01:00
David Baker
ee77820e5b Use latest js-sdk branch 2023-08-23 09:59:02 +01:00
David Baker
7a197a2700 Use js-sdk branch 2023-08-18 14:14:24 +01:00
David Baker
274349f2e2 Use the right sfu config hook 2023-08-18 10:20:14 +01:00
David Baker
e39d00154d More hacking on rtcsession 2023-08-18 09:03:21 +01:00
David Baker
1716bd4418 Merge remote-tracking branch 'origin/livekit' into dbkr/matrixrtcsession 2023-08-16 18:53:00 +01:00
David Baker
4242d45ba2 WIP refactor for removing m.call events 2023-08-16 18:41:27 +01:00
36 changed files with 783 additions and 1182 deletions

View File

@@ -21,5 +21,9 @@ server {
expires 1w;
add_header Cache-Control "public, no-transform";
}
location /apple-app-site-association {
default_type application/json;
}
}

View File

@@ -59,7 +59,7 @@
"i18next-http-backend": "^1.4.4",
"livekit-client": "^1.12.3",
"lodash": "^4.17.21",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#0b72baada1dc9c14fb4ebe1b0595cef0acf4f2c8",
"matrix-widget-api": "^1.3.1",
"mermaid": "^9.0.0",
"normalize.css": "^8.0.1",

View File

@@ -0,0 +1,12 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "io.element.android.x.debug",
"sha256_cert_fingerprints": [
"B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E"
]
}
}
]

View File

@@ -0,0 +1,15 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"7J4U792NQT.io.element.elementx",
"7J4U792NQT.io.element.elementx.nightly",
"7J4U792NQT.io.element.elementx.pr"
],
"paths": ["*"]
}
]
}
}

View File

@@ -1,7 +1,7 @@
{
"{{count, number}}|one": "{{count, number}}",
"{{count, number}}|other": "{{count, number}}",
"{{count}} stars|one": "{{count}} star",
"{{count}} stars|one": "{{count}} stars",
"{{count}} stars|other": "{{count}} stars",
"{{displayName}} is presenting": "{{displayName}} is presenting",
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
@@ -46,7 +46,6 @@
"Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback",
"Fetching group call timed out.": "Fetching group call timed out.",
"Full screen": "Full screen",
"Go": "Go",
"Grid": "Grid",
@@ -54,7 +53,6 @@
"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.",
"Include debug logs": "Include debug logs",
"Incompatible versions": "Incompatible versions",
"Inspector": "Inspector",
"Join call": "Join call",
"Join call now": "Join call now",
@@ -72,7 +70,6 @@
"Not encrypted": "Not encrypted",
"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>",
"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",
"Passwords must match": "Passwords must match",
"Profile": "Profile",
@@ -118,7 +115,6 @@
"Waiting for other participants…": "Waiting for other participants…",
"Walkie-talkie call": "Walkie-talkie call",
"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",
"You were disconnected from the call": "You were disconnected from the call",
"Your feedback": "Your feedback",

View File

@@ -19,10 +19,10 @@ import { Trans } from "react-i18next";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { useEnableE2EE } from "./settings/useSetting";
import { useEnableE2EE } from "./e2ee/e2eeHooks";
export const E2EEBanner = () => {
const [e2eeEnabled] = useEnableE2EE();
const e2eeEnabled = useEnableE2EE();
if (e2eeEnabled) return null;
return (

View File

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

View File

@@ -92,6 +92,10 @@ interface UrlParams {
* E2EE password
*/
password: string | null;
/**
* Whether we the app should use per participant keys for E2EE.
*/
perParticipantE2EE: boolean;
}
/**
@@ -190,6 +194,7 @@ export const getUrlParams = (
fontScale: Number.isNaN(fontScale) ? null : fontScale,
analyticsID: getParam("analyticsID"),
allowIceFallback: hasParam("allowIceFallback"),
perParticipantE2EE: hasParam("perParticipantE2EE"),
};
};

View File

@@ -15,8 +15,9 @@ limitations under the License.
*/
import { useEffect, useMemo } from "react";
import { isE2EESupported } from "livekit-client";
import { useEnableE2EE } from "../settings/useSetting";
import { useEnableSPAE2EE } from "../settings/useSetting";
import { useLocalStorage } from "../useLocalStorage";
import { useClient } from "../ClientContext";
import { PASSWORD_STRING, useUrlParams } from "../UrlParams";
@@ -28,7 +29,7 @@ export const useInternalRoomSharedKey = (
roomId: string
): [string | null, (value: string) => void] => {
const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]);
const [e2eeEnabled] = useEnableE2EE();
const [e2eeEnabled] = useEnableSPAE2EE();
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key);
return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey];
@@ -67,7 +68,7 @@ export const useManageRoomSharedKey = (roomId: string): string | null => {
};
export const useIsRoomE2EE = (roomId: string): boolean | null => {
const { isEmbedded } = useUrlParams();
const { isEmbedded, perParticipantE2EE } = useUrlParams();
const client = useClient();
const room = useMemo(
() => client.client?.getRoom(roomId) ?? null,
@@ -75,11 +76,32 @@ export const useIsRoomE2EE = (roomId: string): boolean | null => {
);
const isE2EE = useMemo(() => {
if (isEmbedded) {
return false;
return perParticipantE2EE;
} else {
return room ? !room?.getCanonicalAlias() : null;
}
}, [isEmbedded, room]);
}, [room, isEmbedded, perParticipantE2EE]);
return isE2EE;
};
export const useEnableEmbeddedE2EE = (): boolean => {
const { isEmbedded, perParticipantE2EE } = useUrlParams();
if (!isEmbedded) return false;
if (!isE2EESupported()) return false;
return perParticipantE2EE;
};
export const useEnableE2EE = (): boolean => {
const [spaE2EEEnabled] = useEnableSPAE2EE();
const embeddedE2EEEnabled = useEnableEmbeddedE2EE();
const e2eeEnabled = useMemo(
() => spaE2EEEnabled || embeddedE2EEEnabled,
[spaE2EEEnabled, embeddedE2EEEnabled]
);
return e2eeEnabled;
};

View File

@@ -0,0 +1,69 @@
/*
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 { BaseKeyProvider, createKeyMaterialFromString } from "livekit-client";
import {
MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
export class MatrixKeyProvider extends BaseKeyProvider {
private rtcSession?: MatrixRTCSession;
public setRTCSession(rtcSession: MatrixRTCSession) {
if (this.rtcSession) {
this.rtcSession.off(
MatrixRTCSessionEvent.EncryptionKeyChanged,
this.onEncryptionKeyChanged
);
}
this.rtcSession = rtcSession;
this.rtcSession.on(
MatrixRTCSessionEvent.EncryptionKeyChanged,
this.onEncryptionKeyChanged
);
// The new session could be aware of keys of which the old session wasn't,
// so emit a key changed event.
for (const [
participant,
encryptionKeys,
] of this.rtcSession.getEncryptionKeys()) {
for (const [index, encryptionKey] of encryptionKeys.entries()) {
this.onEncryptionKeyChanged(encryptionKey, index, participant);
}
}
}
private onEncryptionKeyChanged = async (
encryptionKey: string,
encryptionKeyIndex: number,
participantId: string
) => {
this.onSetEncryptionKey(
await createKeyMaterialFromString(encryptionKey),
participantId,
encryptionKeyIndex
);
console.log(
`Embedded-E2EE-LOG onEncryptionKeyChanged participantId=${participantId} encryptionKeyIndex=${encryptionKeyIndex} encryptionKey=${encryptionKey}`,
this.getKeys()
);
};
}

View File

@@ -24,7 +24,7 @@ import styles from "./CallList.module.css";
import { getRoomUrl } from "../matrix-utils";
import { Body } from "../typography/Typography";
import { GroupCallRoom } from "./useGroupCallRooms";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { useRoomSharedKey } from "../e2ee/e2eeHooks";
interface CallListProps {
rooms: GroupCallRoom[];

View File

@@ -38,11 +38,11 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption, Title } from "../typography/Typography";
import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { useEnableSPAE2EE, useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
import { setLocalStorageItem } from "../useLocalStorage";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/e2eeHooks";
interface Props {
client: MatrixClient;
@@ -57,7 +57,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const [e2eeEnabled] = useEnableE2EE();
const [e2eeEnabled] = useEnableSPAE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => {

View File

@@ -40,10 +40,10 @@ import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { useEnableSPAE2EE, useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/e2eeHooks";
import { setLocalStorageItem } from "../useLocalStorage";
export const UnauthenticatedView: FC = () => {
@@ -60,7 +60,7 @@ export const UnauthenticatedView: FC = () => {
const history = useHistory();
const { t } = useTranslation();
const [e2eeEnabled] = useEnableE2EE();
const [e2eeEnabled] = useEnableSPAE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e) => {

View 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;
}

View File

@@ -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;
};

View File

@@ -14,82 +14,78 @@ See the License for the specific language governing permissions and
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 { 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 {
url: 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
export type OpenIDClientParts = Pick<
MatrixClient,
"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(
client: OpenIDClientParts,
groupCall: GroupCall,
roomName: string
): Promise<SFUConfig> {
activeFocus: LivekitFocus
): Promise<SFUConfig | undefined> {
const openIdToken = await client.getOpenIdToken();
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 {
logger.info(
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`
);
const sfuConfig = await getLiveKitJWT(
client,
urlFromConf,
roomName,
activeFocus.livekit_service_url,
activeFocus.livekit_alias,
openIdToken
);
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.`
);
}
logger.info(`Got JWT from call's active focus URL.`);
return sfuConfig;
} catch (e) {
logger.error("Failed to get JWT from URL defined in Config.", e);
throw e;
logger.warn(
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
e
);
return undefined;
}
}

View 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;
}

View File

@@ -16,16 +16,16 @@ limitations under the License.
import {
ConnectionState,
E2EEOptions,
ExternalE2EEKeyProvider,
Room,
RoomOptions,
setLogLevel,
} from "livekit-client";
import { useConnectionState, useLiveKitRoom } from "@livekit/components-react";
import { useLiveKitRoom } from "@livekit/components-react";
import { useEffect, useMemo, useRef } from "react";
import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { defaultLiveKitOptions } from "./options";
import { SFUConfig } from "./openIDSFU";
@@ -35,34 +35,63 @@ import {
MediaDevices,
useMediaDevices,
} from "./MediaDevicesContext";
import {
ECConnectionState,
useECConnectionState,
} from "./useECConnectionState";
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
export enum E2EEMode {
PerParticipantKey = "per_participant_key",
SharedKey = "shared_key",
}
export type E2EEConfig = {
sharedKey: string;
mode: E2EEMode;
sharedKey?: string;
};
setLogLevel("debug");
interface UseLivekitResult {
livekitRoom?: Room;
connState: ECConnectionState;
}
export function useLiveKit(
muteStates: MuteStates,
rtcSession: MatrixRTCSession,
sfuConfig?: SFUConfig,
e2eeConfig?: E2EEConfig
): Room | undefined {
): UseLivekitResult {
const e2eeOptions = useMemo(() => {
if (!e2eeConfig?.sharedKey) return undefined;
if (!e2eeConfig) return undefined;
return {
keyProvider: new ExternalE2EEKeyProvider(),
worker: new E2EEWorker(),
} as E2EEOptions;
if (e2eeConfig.mode === E2EEMode.PerParticipantKey) {
return {
keyProvider: new MatrixKeyProvider(),
worker: new E2EEWorker(),
};
} else if (e2eeConfig.mode === E2EEMode.SharedKey && e2eeConfig.sharedKey) {
return {
keyProvider: new ExternalE2EEKeyProvider(),
worker: new E2EEWorker(),
};
}
}, [e2eeConfig]);
useEffect(() => {
if (!e2eeConfig?.sharedKey || !e2eeOptions) return;
if (!e2eeOptions) return;
if (!e2eeConfig) return;
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
e2eeConfig?.sharedKey
);
}, [e2eeOptions, e2eeConfig?.sharedKey]);
if (e2eeConfig.mode === E2EEMode.PerParticipantKey) {
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
} else if (e2eeConfig.mode === E2EEMode.SharedKey && e2eeConfig.sharedKey) {
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
e2eeConfig.sharedKey
);
}
}, [e2eeOptions, e2eeConfig, rtcSession]);
const initialMuteStates = useRef<MuteStates>(muteStates);
const devices = useMediaDevices();
@@ -101,7 +130,7 @@ export function useLiveKit(
room: roomWithoutProps,
});
const connectionState = useConnectionState(roomWithoutProps);
const connectionState = useECConnectionState(room, sfuConfig);
useEffect(() => {
// Sync the requested mute states with LiveKit's mute states. We do it this
@@ -149,5 +178,8 @@ export function useLiveKit(
}
}, [room, devices, connectionState]);
return room;
return {
connState: connectionState,
livekitRoom: room,
};
}

View File

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

View File

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

View File

@@ -16,43 +16,46 @@ limitations under the License.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Room } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
import { MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
import { Config } from "../config/Config";
import { MuteStates, useMuteStates } from "./MuteStates";
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
import {
useManageRoomSharedKey,
useIsRoomE2EE,
} from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
useEnableE2EE,
} from "../e2ee/e2eeHooks";
import { useEnableSPAE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useModalTriggerState } from "../Modal";
import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal";
import { E2EEConfig, E2EEMode } from "../livekit/useLiveKit";
import { useEnableEmbeddedE2EE } from "../e2ee/e2eeHooks";
import { useUrlParams } from "../UrlParams";
declare global {
interface Window {
groupCall?: GroupCall;
rtcSession?: MatrixRTCSession;
}
}
@@ -62,7 +65,7 @@ interface Props {
isEmbedded: boolean;
preload: boolean;
hideHeader: boolean;
groupCall: GroupCall;
rtcSession: MatrixRTCSession;
}
export function GroupCallView({
@@ -71,43 +74,47 @@ export function GroupCallView({
isEmbedded,
preload,
hideHeader,
groupCall,
rtcSession,
}: Props) {
const { state, error, enter, leave, participants, otelGroupCallMembership } =
useGroupCall(groupCall, client);
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
const e2eeSharedKey = useManageRoomSharedKey(rtcSession.room.roomId);
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
const [spaE2EEEnabled] = useEnableSPAE2EE();
const embeddedE2EEEnabled = useEnableEmbeddedE2EE();
const e2eeEnabled = useEnableE2EE();
const { perParticipantE2EE } = useUrlParams();
const { t } = useTranslation();
useEffect(() => {
window.groupCall = groupCall;
window.rtcSession = rtcSession;
return () => {
delete window.groupCall;
delete window.rtcSession;
};
}, [groupCall]);
}, [rtcSession]);
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(groupCall.room);
const roomAvatar = useRoomAvatar(groupCall.room);
const roomEncrypted = useIsRoomE2EE(groupCall.room.roomId)!;
const roomName = useRoomName(rtcSession.room);
const roomAvatar = useRoomAvatar(rtcSession.room);
const roomEncrypted = useIsRoomE2EE(rtcSession.room.roomId)!;
const matrixInfo = useMemo((): MatrixInfo => {
return {
userId: client.getUserId()!,
displayName: displayName!,
avatarUrl: avatarUrl!,
roomId: groupCall.room.roomId,
roomId: rtcSession.room.roomId,
roomName,
roomAlias: groupCall.room.getCanonicalAlias(),
roomAlias: rtcSession.room.getCanonicalAlias(),
roomAvatar,
roomEncrypted,
};
}, [
displayName,
avatarUrl,
groupCall,
rtcSession,
roomName,
roomAvatar,
roomEncrypted,
@@ -116,18 +123,22 @@ export function GroupCallView({
const participatingMembers = useMemo(() => {
const members: RoomMember[] = [];
for (const [member, deviceMap] of participants.entries()) {
// Count each member only once, regardless of how many devices they use
if (deviceMap.size > 0) members.push(member);
// Count each member only once, regardless of how many devices they use
const addedUserIds = new Set<string>();
for (const membership of memberships) {
if (!addedUserIds.has(membership.member.userId)) {
addedUserIds.add(membership.member.userId);
members.push(membership.member);
}
}
return members;
}, [participants]);
}, [memberships]);
const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices>();
latestDevices.current = deviceContext;
const muteStates = useMuteStates(participants.size);
const muteStates = useMuteStates(memberships.length);
const latestMuteStates = useRef<MuteStates>();
latestMuteStates.current = muteStates;
@@ -184,10 +195,13 @@ export function GroupCallView({
}
}
await enter();
enterRTCSession(rtcSession, embeddedE2EEEnabled);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
// we only have room sessions right now, so call ID is the emprty string - we use the room ID
PosthogAnalytics.instance.eventCallStarted.track(
rtcSession.room.roomId
);
await Promise.all([
widget!.api.setAlwaysOnScreen(true),
@@ -200,19 +214,18 @@ export function GroupCallView({
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
}
}, [groupCall, preload, enter]);
}, [rtcSession, preload, embeddedE2EEEnabled]);
useEffect(() => {
if (isEmbedded && !preload) {
// In embedded mode, bypass the lobby and just enter the call straight away
enter();
enterRTCSession(rtcSession, embeddedE2EEEnabled);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
// use the room ID as above
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
}
}, [groupCall, isEmbedded, preload, enter]);
useSentryGroupCallHandler(groupCall);
}, [rtcSession, isEmbedded, preload, embeddedE2EEEnabled]);
const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
@@ -223,21 +236,16 @@ export function GroupCallView({
setLeaveError(leaveError);
setLeft(true);
let participantCount = 0;
for (const deviceMap of groupCall.participants.values()) {
participantCount += deviceMap.size;
}
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
// therefore we want the event to be sent instantly without getting queued/batched.
const sendInstantly = !!widget;
PosthogAnalytics.instance.eventCallEnded.track(
groupCall.groupCallId,
participantCount,
rtcSession.room.roomId,
rtcSession.memberships.length,
sendInstantly
);
leave();
leaveRTCSession(rtcSession);
if (widget) {
// we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked.
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
@@ -254,13 +262,13 @@ export function GroupCallView({
history.push("/");
}
},
[groupCall, leave, isPasswordlessUser, isEmbedded, history]
[rtcSession, isPasswordlessUser, isEmbedded, history]
);
useEffect(() => {
if (widget && state === GroupCallState.Entered) {
if (widget && isJoined) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leave();
leaveRTCSession(rtcSession);
await widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false);
};
@@ -269,22 +277,23 @@ export function GroupCallView({
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
};
}
}, [groupCall, state, leave]);
}, [isJoined, rtcSession]);
const [e2eeEnabled] = useEnableE2EE();
const e2eeConfig = useMemo(
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
[e2eeSharedKey]
);
const e2eeConfig = useMemo((): E2EEConfig | undefined => {
if (perParticipantE2EE) {
return { mode: E2EEMode.PerParticipantKey };
} else if (e2eeSharedKey) {
return { mode: E2EEMode.SharedKey, sharedKey: e2eeSharedKey };
}
}, [perParticipantE2EE, e2eeSharedKey]);
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
groupCall.enter();
}, [groupCall]);
enterRTCSession(rtcSession, embeddedE2EEEnabled);
}, [rtcSession, embeddedE2EEEnabled]);
const joinRule = useJoinRule(groupCall.room);
const joinRule = useJoinRule(rtcSession.room);
const { modalState: shareModalState, modalProps: shareModalProps } =
useModalTriggerState();
@@ -295,7 +304,7 @@ export function GroupCallView({
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
if (spaE2EEEnabled && isRoomE2EE && !e2eeSharedKey) {
return (
<ErrorView
error={
@@ -311,40 +320,27 @@ export function GroupCallView({
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 && (
<ShareModal roomId={groupCall.room.roomId} {...shareModalProps} />
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
);
if (error) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
if (isJoined) {
return (
<OpenIDLoader
client={client}
groupCall={groupCall}
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
>
<>
{shareModal}
<ActiveCall
client={client}
matrixInfo={matrixInfo}
groupCall={groupCall}
participants={participants}
rtcSession={rtcSession}
participatingMembers={participatingMembers}
onLeave={onLeave}
hideHeader={hideHeader}
muteStates={muteStates}
e2eeConfig={e2eeConfig}
otelGroupCallMembership={otelGroupCallMembership}
//otelGroupCallMembership={otelGroupCallMembership}
onShareClick={onShareClick}
/>
</OpenIDLoader>
</>
);
} else if (left) {
// The call ended view is shown for two reasons: prompting guests to create
@@ -360,7 +356,7 @@ export function GroupCallView({
) {
return (
<CallEndedView
endedCallId={groupCall.groupCallId}
endedCallId={rtcSession.room.roomId}
client={client}
isPasswordlessUser={isPasswordlessUser}
leaveError={leaveError}
@@ -389,7 +385,7 @@ export function GroupCallView({
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => enter()}
onEnter={() => enterRTCSession(rtcSession, embeddedE2EEEnabled)}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
participatingMembers={participatingMembers}

View File

@@ -23,16 +23,16 @@ import {
useTracks,
} from "@livekit/components-react";
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 { 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 { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
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 LogoType } from "../icons/LogoType.svg";
@@ -50,19 +50,14 @@ import {
TileDescriptor,
VideoGrid,
} from "../video-grid/VideoGrid";
import {
useShowInspector,
useShowConnectionStats,
} from "../settings/useSetting";
import { useShowConnectionStats } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ElementWidgetActions, widget } from "../widget";
import { GroupCallInspector } from "./GroupCallInspector";
import styles from "./InCallView.module.css";
import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
@@ -72,14 +67,14 @@ import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader";
import { useEventEmitterThree } from "../useEvents";
import { useWakeLock } from "../useWakeLock";
import { useMergedRefs } from "../useMergedRefs";
import { MuteStates } from "./MuteStates";
import { useOpenIDSFU } from "../livekit/openIDSFU";
import { MatrixInfo } from "./VideoPreview";
import { ShareButton } from "../button/ShareButton";
import { LayoutToggle } from "./LayoutToggle";
import { ECConnectionState } from "../livekit/useECConnectionState";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -87,13 +82,19 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// For now we can disable screensharing in Safari.
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;
}
export function ActiveCall(props: ActiveCallProps) {
const sfuConfig = useSFUConfig();
const livekitRoom = useLiveKit(props.muteStates, sfuConfig, props.e2eeConfig);
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
const { livekitRoom, connState } = useLiveKit(
props.muteStates,
props.rtcSession,
sfuConfig,
props.e2eeConfig
);
if (!livekitRoom) {
return null;
@@ -105,7 +106,7 @@ export function ActiveCall(props: ActiveCallProps) {
return (
<RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} />
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
</RoomContext.Provider>
);
}
@@ -113,34 +114,42 @@ export function ActiveCall(props: ActiveCallProps) {
export interface InCallViewProps {
client: MatrixClient;
matrixInfo: MatrixInfo;
groupCall: GroupCall;
rtcSession: MatrixRTCSession;
livekitRoom: Room;
muteStates: MuteStates;
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
participatingMembers: RoomMember[];
onLeave: (error?: Error) => void;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
connState: ECConnectionState;
onShareClick: (() => void) | null;
}
export function InCallView({
client,
matrixInfo,
groupCall,
rtcSession,
livekitRoom,
muteStates,
participants,
participatingMembers,
onLeave,
hideHeader,
otelGroupCallMembership,
connState,
onShareClick,
}: InCallViewProps) {
const { t } = useTranslation();
usePreventScroll();
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 [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
const boundsValid = bounds.height > 0;
@@ -157,7 +166,7 @@ export function InCallView({
screenSharingTracks.length > 0
);
const [showInspector] = useShowInspector();
//const [showInspector] = useShowInspector();
const [showConnectionStats] = useShowConnectionStats();
const { hideScreensharing } = useUrlParams();
@@ -166,14 +175,12 @@ export function InCallView({
room: livekitRoom,
});
const toggleMicrophone = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e),
[muteStates]
);
const toggleCamera = useCallback(
() => muteStates.video.setEnabled?.((e) => !e),
[muteStates]
);
const toggleMicrophone = useCallback(() => {
muteStates.audio.setEnabled?.((e) => !e);
}, [muteStates]);
const toggleCamera = useCallback(() => {
muteStates.video.setEnabled?.((e) => !e);
}, [muteStates]);
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
@@ -184,27 +191,10 @@ export function InCallView({
(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(() => {
onLeave();
}, [onLeave]);
useEventEmitterThree<RoomEvent.Disconnected, RoomEventCallbacks>(
livekitRoom,
RoomEvent.Disconnected,
onDisconnected
);
useEffect(() => {
widget?.api.transport.send(
layout === "grid"
@@ -245,7 +235,7 @@ export function InCallView({
const reducedControls = boundsValid && bounds.width <= 340;
const noControls = reducedControls && bounds.height <= 400;
const items = useParticipantTiles(livekitRoom, participants);
const items = useParticipantTiles(livekitRoom, rtcSession.room);
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
useFullscreen(items);
@@ -319,7 +309,7 @@ export function InCallView({
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(groupCall.room.roomId);
} = useRageshakeRequestModal(rtcSession.room.roomId);
const {
modalState: settingsModalState,
@@ -433,24 +423,24 @@ export function InCallView({
{renderContent()}
{footer}
</div>
{otelGroupCallMembership && (
{/*otelGroupCallMembership && (
<GroupCallInspector
client={client}
groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership}
show={showInspector}
/>
)}
)*/}
{rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomId={groupCall.room.roomId}
roomId={rtcSession.room.roomId}
/>
)}
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={groupCall.room.roomId}
roomId={rtcSession.room.roomId}
{...settingsModalProps}
/>
)}
@@ -458,25 +448,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(
livekitRoom: Room,
participants: Map<RoomMember, Map<string, ParticipantInfo>>
matrixRoom: MatrixRoom
): TileDescriptor<ItemData>[] {
const sfuParticipants = useParticipants({
room: livekitRoom,
});
const items = useMemo(() => {
// The IDs of the participants who published membership event to the room (i.e. are present from Matrix perspective).
const matrixParticipants: Map<string, RoomMember> = new Map(
[...participants.entries()].flatMap(([user, devicesMap]) => {
return [...devicesMap.keys()].map((deviceId) => [
`${user.userId}:${deviceId}`,
user,
]);
})
);
const hasPresenter =
sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined;
let allGhosts = true;
@@ -492,7 +493,14 @@ function useParticipantTiles(
: false;
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;
const userMediaTile = {
@@ -544,7 +552,7 @@ function useParticipantTiles(
// If every item is a ghost, that probably means we're still connecting and
// shouldn't bother showing anything yet
return allGhosts ? [] : tiles;
}, [participants, sfuParticipants]);
}, [matrixRoom, sfuParticipants]);
return items;
}

View File

@@ -26,7 +26,7 @@ import { Body, Link } from "../typography/Typography";
import { useLocationNavigation } from "../useLocationNavigation";
import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { MuteStates } from "./MuteStates";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { useRoomSharedKey } from "../e2ee/e2eeHooks";
import { ShareButton } from "../button/ShareButton";
interface Props {

View File

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

View File

@@ -21,7 +21,7 @@ import { Modal, ModalContent, ModalProps } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { useRoomSharedKey } from "../e2ee/e2eeHooks";
interface Props extends Omit<ModalProps, "title" | "children"> {
roomId: string;

View 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;
}

View File

@@ -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,
};
}

View File

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

View File

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

56
src/rtcSessionHelpers.ts Normal file
View File

@@ -0,0 +1,56 @@
/*
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,
encryptMedia: boolean
) {
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)], encryptMedia);
}
export function leaveRTCSession(rtcSession: MatrixRTCSession) {
//groupCallOTelMembership?.onLeaveCall();
rtcSession.leaveRoomSession();
}

View File

@@ -34,7 +34,7 @@ import {
useOptInAnalytics,
useDeveloperSettingsTab,
useShowConnectionStats,
useEnableE2EE,
useEnableSPAE2EE,
} from "./useSetting";
import { FieldRow, InputField } from "../input/Input";
import { Button } from "../button";
@@ -69,7 +69,7 @@ export const SettingsModal = (props: Props) => {
useDeveloperSettingsTab();
const [showConnectionStats, setShowConnectionStats] =
useShowConnectionStats();
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
const [enableE2EE, setEnableE2EE] = useEnableSPAE2EE();
const downloadDebugLog = useDownloadDebugLog();

View File

@@ -23,6 +23,7 @@ import {
setLocalStorageItem,
useLocalStorage,
} from "../useLocalStorage";
import { useEnableEmbeddedE2EE } from "../e2ee/e2eeHooks";
type Setting<T> = [T, (value: T) => void];
type DisableableSetting<T> = [T, ((value: T) => void) | null];
@@ -89,14 +90,17 @@ export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
return [false, null];
};
export const useEnableE2EE = (): DisableableSetting<boolean | null> => {
export const useEnableSPAE2EE = (): DisableableSetting<boolean | null> => {
const embeddedE2EEEnabled = useEnableEmbeddedE2EE();
const settingVal = useSetting<boolean | null>(
"enable-end-to-end-encryption",
true
);
if (isE2EESupported()) return settingVal;
return [false, null];
if (embeddedE2EEEnabled) return [false, null];
if (!isE2EESupported()) return [false, null];
return settingVal;
};
export const useDeveloperSettingsTab = () =>

View 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;
}

View 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;
}

View File

@@ -23,7 +23,6 @@ import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter";
import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
// Subset of the actions in matrix-react-sdk
export enum ElementWidgetActions {
@@ -79,6 +78,8 @@ export const widget: WidgetHelpers | null = (() => {
logger.info("Widget API is available");
const api = new WidgetApi(widgetId, parentOrigin);
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
api.requestCapabilityToSendEvent(EventType.CallEncryptionPrefix);
api.requestCapabilityToReceiveEvent(EventType.CallEncryptionPrefix);
// Set up the lazy action emitter, but only for select actions that we
// intend for the app to handle
@@ -157,30 +158,11 @@ export const widget: WidgetHelpers | null = (() => {
timelineSupport: true,
useE2eForGroupCall: e2eEnabled,
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,
}
);
const clientPromise = new Promise<MatrixClient>((resolve) => {
(async () => {
// wait for the config file to be ready (we load very early on so it might not
// be otherwise)
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();
resolve(client);
})();

View File

@@ -1826,15 +1826,7 @@
dependencies:
"@floating-ui/utils" "^0.1.1"
"@floating-ui/dom@^1.1.0":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.1.tgz#88b70defd002fe851f17b4a25efb2d3c04d7a8d7"
integrity sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==
dependencies:
"@floating-ui/core" "^1.4.1"
"@floating-ui/utils" "^0.1.1"
"@floating-ui/dom@^1.5.1":
"@floating-ui/dom@^1.1.0", "@floating-ui/dom@^1.5.1":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.2.tgz#6812e89d1d4d4ea32f10d15c3b81feb7f9836d89"
integrity sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog==
@@ -2287,19 +2279,19 @@
rxjs "^7.8.0"
"@livekit/components-react@^1.1.0":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-1.1.6.tgz#4e832570bd3fb0c15e36ca7513f736683de62fd4"
integrity sha512-wA5wKVsKM4cBskXkTbgQ8UhEWCq7hYn/ElOlm9IsGf0U3KpsXpyiW/h2hd/aDr/ufTbBWYjtPWa8RrvoBImnyg==
version "1.1.8"
resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-1.1.8.tgz#34f67a6646bcd5c44d87d3fed5be5010b297f111"
integrity sha512-Ljlcqkgg7IqdQIDr7/ViEsL8GZkmSkoMC38hz3CkCcmHP+e8U4+4be2C2feiHb4hHw+tLd1DPuElyEWuTeiQKQ==
dependencies:
"@livekit/components-core" "0.6.15"
"@react-hook/latest" "^1.0.3"
clsx "^2.0.0"
usehooks-ts "^2.9.1"
"@matrix-org/matrix-sdk-crypto-js@^0.1.1":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.4.tgz#c13c7c8c3a1d8da08e6ad195d25e5e61cc402df7"
integrity sha512-OxG84iSeR89zYLFeb+DCaFtZT+DDiIu+kTkqY8OYfhE5vpGLFX2sDVBRrAdos1IUqEoboDloDBR9+yU7hNRyog==
"@matrix-org/matrix-sdk-crypto-wasm@^1.2.3-alpha.0":
version "1.2.3-alpha.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-1.2.3-alpha.0.tgz#f6f93e3ee44c5f1e0e255badd26f4a7d3fb1dab8"
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":
version "3.2.14"
@@ -11632,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"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9":
version "26.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b698217445318f453e0b1086364a33113eaa85d9"
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#0b72baada1dc9c14fb4ebe1b0595cef0acf4f2c8":
version "28.1.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0b72baada1dc9c14fb4ebe1b0595cef0acf4f2c8"
dependencies:
"@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"
bs58 "^5.0.0"
content-type "^1.0.4"
jwt-decode "^3.1.2"
loglevel "^1.7.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"
p-retry "4"
sdp-transform "^2.14.1"
unhomoglyph "^1.0.6"
uuid "9"
matrix-widget-api@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz#e426ec16a013897f3a4a9c2bff423f54ab0ba745"
integrity sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A==
matrix-widget-api@^1.3.1, matrix-widget-api@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.6.0.tgz#f0075411edffc6de339580ade7e6e6e6edb01af4"
integrity sha512-VXIJyAZ/WnBmT4C7ePqevgMYGneKMCP/0JuCOqntSsaNlCRHJvwvTxmqUU+ufOpzIF5gYNyIrAjbgrEbK3iqJQ==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
@@ -15780,7 +15772,7 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@9, uuid@^9.0.0:
uuid@9:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
@@ -15795,6 +15787,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"