diff --git a/package.json b/package.json index 58b17ce6..acc6068d 100644 --- a/package.json +++ b/package.json @@ -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#6836720e1e1c2cb01d49d6e5fcfc01afc14834ca", "matrix-widget-api": "^1.3.1", "mermaid": "^9.0.0", "normalize.css": "^8.0.1", diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 69127826..c3899a65 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -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": "Not registered yet? <2>Create an account", - "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}", "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", diff --git a/src/IncompatibleVersionModal.tsx b/src/IncompatibleVersionModal.tsx deleted file mode 100644 index d11822de..00000000 --- a/src/IncompatibleVersionModal.tsx +++ /dev/null @@ -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; - room: Room; - onClose: () => void; -} - -export const IncompatibleVersionModal: FC = ({ - userIds, - room, - onClose, - ...rest -}) => { - const { t } = useTranslation(); - const userLis = useMemo( - () => [...userIds].map((u) =>
  • {room.getMember(u)?.name ?? u}
  • ), - [userIds, room] - ); - - return ( - - - - - Other users are trying to join this call from incompatible versions. - These users should ensure that they have refreshed their browsers: -
      {userLis}
    -
    - -
    -
    - ); -}; diff --git a/src/livekit/LivekitFocus.ts b/src/livekit/LivekitFocus.ts new file mode 100644 index 00000000..f3a1e532 --- /dev/null +++ b/src/livekit/LivekitFocus.ts @@ -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; +} diff --git a/src/livekit/OpenIDLoader.tsx b/src/livekit/OpenIDLoader.tsx deleted file mode 100644 index 911a1ae7..00000000 --- a/src/livekit/OpenIDLoader.tsx +++ /dev/null @@ -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(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 ; - case "failed": - return ; - case "loaded": - return ( - - {children} - - ); - } -} - -type SFUConfigLoading = { - kind: "loading"; -}; - -type SFUConfigLoaded = { - kind: "loaded"; - sfuConfig: SFUConfig; -}; - -type SFUConfigFailed = { - kind: "failed"; - error: Error; -}; diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index 083e95b2..d10f56fb 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -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(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 { + activeFocus: LivekitFocus +): Promise { 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; } } diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts new file mode 100644 index 00000000..01adb64d --- /dev/null +++ b/src/livekit/useECConnectionState.ts @@ -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; +} diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 14acfff3..1f5704b3 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -22,7 +22,7 @@ import { 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"; @@ -35,6 +35,10 @@ import { MediaDevices, useMediaDevices, } from "./MediaDevicesContext"; +import { + ECConnectionState, + useECConnectionState, +} from "./useECConnectionState"; export type E2EEConfig = { sharedKey: string; @@ -42,11 +46,16 @@ export type E2EEConfig = { setLogLevel("debug"); +interface UseLivekitResult { + livekitRoom?: Room; + connState: ECConnectionState; +} + export function useLiveKit( muteStates: MuteStates, sfuConfig?: SFUConfig, e2eeConfig?: E2EEConfig -): Room | undefined { +): UseLivekitResult { const e2eeOptions = useMemo(() => { if (!e2eeConfig?.sharedKey) return undefined; @@ -101,7 +110,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 +158,8 @@ export function useLiveKit( } }, [room, devices, connectionState]); - return room; + return { + connState: connectionState, + livekitRoom: room, + }; } diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index d074fa3f..aed343a5 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -172,7 +172,6 @@ export async function initClient( localTimeoutMs: 5000, useE2eForGroupCall: e2eEnabled, fallbackICEServerAllowed: fallbackICEServerAllowed, - useLivekitForGroupCalls: true, }); try { diff --git a/src/room/GroupCallLoader.tsx b/src/room/GroupCallLoader.tsx index ca52a163..78fa780c 100644 --- a/src/room/GroupCallLoader.tsx +++ b/src/room/GroupCallLoader.tsx @@ -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({ ); case "loaded": - return <>{children(groupCallState.groupCall)}; + return <>{children(groupCallState.rtcSession)}; case "failed": return ; } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index f798b7d8..ac0ae17d 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -16,29 +16,28 @@ 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, @@ -52,7 +51,7 @@ import { ShareModal } from "./ShareModal"; declare global { interface Window { - groupCall?: GroupCall; + rtcSession?: MatrixRTCSession; } } @@ -62,7 +61,7 @@ interface Props { isEmbedded: boolean; preload: boolean; hideHeader: boolean; - groupCall: GroupCall; + rtcSession: MatrixRTCSession; } export function GroupCallView({ @@ -71,43 +70,43 @@ 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 { 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 +115,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(); + 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(); latestDevices.current = deviceContext; - const muteStates = useMuteStates(participants.size); + const muteStates = useMuteStates(memberships.length); const latestMuteStates = useRef(); latestMuteStates.current = muteStates; @@ -184,10 +187,13 @@ export function GroupCallView({ } } - await enter(); + enterRTCSession(rtcSession); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); - PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); + // we only have room sessions right now, so call ID is the emprty string - we use the room ID + PosthogAnalytics.instance.eventCallStarted.track( + rtcSession.room.roomId + ); await Promise.all([ widget!.api.setAlwaysOnScreen(true), @@ -200,19 +206,18 @@ export function GroupCallView({ widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin); }; } - }, [groupCall, preload, enter]); + }, [rtcSession, preload]); useEffect(() => { if (isEmbedded && !preload) { // In embedded mode, bypass the lobby and just enter the call straight away - enter(); + enterRTCSession(rtcSession); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); - PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); + // use the room ID as above + PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); } - }, [groupCall, isEmbedded, preload, enter]); - - useSentryGroupCallHandler(groupCall); + }, [rtcSession, isEmbedded, preload]); const [left, setLeft] = useState(false); const [leaveError, setLeaveError] = useState(undefined); @@ -223,21 +228,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 +254,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) => { - leave(); + leaveRTCSession(rtcSession); await widget!.api.transport.reply(ev.detail, {}); widget!.api.setAlwaysOnScreen(false); }; @@ -269,7 +269,7 @@ export function GroupCallView({ widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup); }; } - }, [groupCall, state, leave]); + }, [isJoined, rtcSession]); const [e2eeEnabled] = useEnableE2EE(); @@ -281,10 +281,10 @@ export function GroupCallView({ const onReconnect = useCallback(() => { setLeft(false); setLeaveError(undefined); - groupCall.enter(); - }, [groupCall]); + enterRTCSession(rtcSession); + }, [rtcSession]); - const joinRule = useJoinRule(groupCall.room); + const joinRule = useJoinRule(rtcSession.room); const { modalState: shareModalState, modalProps: shareModalProps } = useModalTriggerState(); @@ -311,40 +311,27 @@ export function GroupCallView({ return ; } - const livekitServiceURL = - groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url; - if (!livekitServiceURL) { - return ; - } - const shareModal = shareModalState.isOpen && ( - + ); - if (error) { - return ; - } else if (state === GroupCallState.Entered) { + if (isJoined) { return ( - + <> {shareModal} - + ); } else if (left) { // The call ended view is shown for two reasons: prompting guests to create @@ -360,7 +347,7 @@ export function GroupCallView({ ) { return ( enter()} + onEnter={() => enterRTCSession(rtcSession)} isEmbedded={isEmbedded} hideHeader={hideHeader} participatingMembers={participatingMembers} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index e67c3043..4ef91fe5 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -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 { MatrixInfo } from "./VideoPreview"; import { ShareButton } from "../button/ShareButton"; import { LayoutToggle } from "./LayoutToggle"; +import { ECConnectionState } from "../livekit/useECConnectionState"; +import { useOpenIDSFU } from "../livekit/openIDSFU"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -87,13 +82,18 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // For now we can disable screensharing in Safari. const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); -export interface ActiveCallProps extends Omit { +export interface ActiveCallProps + extends Omit { 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, + sfuConfig, + props.e2eeConfig + ); if (!livekitRoom) { return null; @@ -105,7 +105,7 @@ export function ActiveCall(props: ActiveCallProps) { return ( - + ); } @@ -113,34 +113,42 @@ export function ActiveCall(props: ActiveCallProps) { export interface InCallViewProps { client: MatrixClient; matrixInfo: MatrixInfo; - groupCall: GroupCall; + rtcSession: MatrixRTCSession; livekitRoom: Room; muteStates: MuteStates; - participants: Map>; 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(null); const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver }); const boundsValid = bounds.height > 0; @@ -157,7 +165,7 @@ export function InCallView({ screenSharingTracks.length > 0 ); - const [showInspector] = useShowInspector(); + //const [showInspector] = useShowInspector(); const [showConnectionStats] = useShowConnectionStats(); const { hideScreensharing } = useUrlParams(); @@ -184,27 +192,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( - livekitRoom, - RoomEvent.Disconnected, - onDisconnected - ); - useEffect(() => { widget?.api.transport.send( layout === "grid" @@ -245,7 +236,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 +310,7 @@ export function InCallView({ const { modalState: rageshakeRequestModalState, modalProps: rageshakeRequestModalProps, - } = useRageshakeRequestModal(groupCall.room.roomId); + } = useRageshakeRequestModal(rtcSession.room.roomId); const { modalState: settingsModalState, @@ -433,24 +424,24 @@ export function InCallView({ {renderContent()} {footer} - {otelGroupCallMembership && ( + {/*otelGroupCallMembership && ( - )} + )*/} {rageshakeRequestModalState.isOpen && !noControls && ( )} {settingsModalState.isOpen && ( )} @@ -458,25 +449,36 @@ export function InCallView({ ); } +function findMatrixMember( + room: MatrixRoom, + id: string +): RoomMember | undefined { + if (!id) return undefined; + + const parts = id.split(":"); + // must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon + if (parts.length < 3) { + logger.warn( + "Livekit participants ID doesn't look like a userId:deviceId combination" + ); + return undefined; + } + + parts.pop(); + const userId = parts.join(":"); + + return room.getMember(userId) ?? undefined; +} + function useParticipantTiles( livekitRoom: Room, - participants: Map> + matrixRoom: MatrixRoom ): TileDescriptor[] { 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 = 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 +494,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 +553,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; } diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index ddf92959..50156e70 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -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) => ( + 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; +} diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts deleted file mode 100644 index 26aa64fb..00000000 --- a/src/room/useGroupCall.ts +++ /dev/null @@ -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; - leave: () => void; - toggleLocalVideoMuted: () => void; - toggleMicrophoneMuted: () => void; - toggleScreensharing: () => void; - setMicrophoneMuted: (muted: boolean) => void; - requestingScreenshare: boolean; - isScreensharing: boolean; - screenshareFeeds: CallFeed[]; - participants: Map>; - 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>; - 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> { - const participants = new Map>(); - - for (const [member, participantsStateMap] of groupCall.participants) { - const participantInfoMap = new Map(); - 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: 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) => 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(); - - 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(); - - 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 - ): void { - groupCallOTelMembership?.onConnectionStatsReport(report); - } - - function onByteSentStatsReport( - report: GroupCallStatsReport - ): void { - groupCallOTelMembership?.onByteSentStatsReport(report); - } - - function onSummaryStatsReport( - report: GroupCallStatsReport - ): void { - groupCallOTelMembership?.onSummaryStatsReport(report); - } - - function onCallFeedStatsReport( - report: GroupCallStatsReport - ): 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) => { - 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) => { - 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, - }; -} diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 3436360c..9218bf32 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -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; - export type GroupCallLoaded = { kind: "loaded"; - groupCall: GroupCall; + rtcSession: MatrixRTCSession; }; export type GroupCallLoadFailed = { @@ -130,61 +121,12 @@ export const useLoadGroupCall = ( } }; - const fetchOrCreateGroupCall = async (): Promise => { + const fetchOrCreateGroupCall = async (): Promise => { 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]); diff --git a/src/room/useSentryGroupCallHandler.ts b/src/room/useSentryGroupCallHandler.ts deleted file mode 100644 index 188a2934..00000000 --- a/src/room/useSentryGroupCallHandler.ts +++ /dev/null @@ -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]); -} diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts new file mode 100644 index 00000000..3d62f980 --- /dev/null +++ b/src/rtcSessionHelpers.ts @@ -0,0 +1,53 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; + +import { PosthogAnalytics } from "./analytics/PosthogAnalytics"; +import { LivekitFocus } from "./livekit/LivekitFocus"; +import { Config } from "./config/Config"; + +function makeFocus(livekitAlias: string): LivekitFocus { + const urlFromConf = Config.get().livekit!.livekit_service_url; + if (!urlFromConf) { + throw new Error("No livekit_service_url is configured!"); + } + + return { + type: "livekit", + livekit_service_url: urlFromConf, + livekit_alias: livekitAlias, + }; +} + +export function enterRTCSession(rtcSession: MatrixRTCSession) { + PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); + PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); + + // This must be called before we start trying to join the call, as we need to + // have started tracking by the time calls start getting created. + //groupCallOTelMembership?.onJoinCall(); + + // right now we asume everything is a room-scoped call + const livekitAlias = rtcSession.room.roomId; + + rtcSession.joinRoomSession([makeFocus(livekitAlias)]); +} + +export function leaveRTCSession(rtcSession: MatrixRTCSession) { + //groupCallOTelMembership?.onLeaveCall(); + rtcSession.leaveRoomSession(); +} diff --git a/src/useMatrixRTCSessionJoinState.ts b/src/useMatrixRTCSessionJoinState.ts new file mode 100644 index 00000000..c7c54563 --- /dev/null +++ b/src/useMatrixRTCSessionJoinState.ts @@ -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; +} diff --git a/src/useMatrixRTCSessionMemberships.ts b/src/useMatrixRTCSessionMemberships.ts new file mode 100644 index 00000000..a0d5a513 --- /dev/null +++ b/src/useMatrixRTCSessionMemberships.ts @@ -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; +} diff --git a/src/widget.ts b/src/widget.ts index d36a3053..d02c5893 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -157,15 +157,6 @@ 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, } ); @@ -174,13 +165,6 @@ export const widget: WidgetHelpers | null = (() => { // wait for the config file to be ready (we load very early on so it might not // 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); })(); diff --git a/yarn.lock b/yarn.lock index a97af500..4299ae8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2288,10 +2288,10 @@ 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" @@ -11624,29 +11624,29 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" 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#6836720e1e1c2cb01d49d6e5fcfc01afc14834ca": + version "28.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6836720e1e1c2cb01d49d6e5fcfc01afc14834ca" 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"