From 4cd274b91eb6cf165926a5198d9178f85945de6a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 29 Aug 2023 12:44:30 +0100 Subject: [PATCH] Manually disconnect & reconnect the livekit call if our focus changes Without breaking the 'disconnected' screen --- src/livekit/openIDSFU.ts | 7 +++ src/livekit/useECConnectionState.ts | 90 +++++++++++++++++++++++++++++ src/livekit/useLiveKit.ts | 20 +++++-- src/room/InCallView.tsx | 45 +++++++-------- 4 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 src/livekit/useECConnectionState.ts diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index c122a844..d10f56fb 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -27,6 +27,13 @@ export interface SFUConfig { 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, diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts new file mode 100644 index 00000000..2e8bb430 --- /dev/null +++ b/src/livekit/useECConnectionState.ts @@ -0,0 +1,90 @@ +/* +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; + +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)); + + useEffect(() => { + if ( + sfuConfig && + currentSFUConfig.current && + !sfuConfigEquals(currentSFUConfig.current, sfuConfig) + ) { + logger.info("JWT changed!"); + + (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/room/InCallView.tsx b/src/room/InCallView.tsx index e5f56668..76cb7c07 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -24,7 +24,7 @@ import { } from "@livekit/components-react"; import { usePreventScroll } from "@react-aria/overlays"; import classNames from "classnames"; -import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client"; +import { Room, Track, ConnectionState } from "livekit-client"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room"; @@ -34,7 +34,6 @@ import useMeasure from "react-use-measure"; import { OverlayTriggerState } from "@react-stately/overlays"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { logger } from "matrix-js-sdk/src/logger"; -import { RoomEventCallbacks } from "livekit-client/dist/src/room/Room"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership"; @@ -74,12 +73,12 @@ import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; import { E2EELock } from "../E2EELock"; -import { useEventEmitterThree } from "../useEvents"; import { useWakeLock } from "../useWakeLock"; import { useMergedRefs } from "../useMergedRefs"; import { MuteStates } from "./MuteStates"; import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement"; import { useOpenIDSFU } from "../livekit/openIDSFU"; +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 +86,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 = useOpenIDSFU(props.client, props.rtcSession); - const livekitRoom = useLiveKit(props.muteStates, sfuConfig, props.e2eeConfig); + const { livekitRoom, connState } = useLiveKit( + props.muteStates, + sfuConfig, + props.e2eeConfig + ); if (!livekitRoom) { return null; @@ -105,7 +109,7 @@ export function ActiveCall(props: ActiveCallProps) { return ( - + ); } @@ -119,6 +123,7 @@ export interface InCallViewProps { onLeave: (error?: Error) => void; hideHeader: boolean; otelGroupCallMembership?: OTelGroupCallMembership; + connState: ECConnectionState; } export function InCallView({ @@ -130,11 +135,20 @@ export function InCallView({ onLeave, hideHeader, otelGroupCallMembership, + connState, }: 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 isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId); const containerRef1 = useRef(null); @@ -182,27 +196,10 @@ export function InCallView({ async (muted) => await localParticipant.setMicrophoneEnabled(!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 === "freedom" @@ -459,6 +456,8 @@ function findMatrixMember( room: MatrixRoom, id: string ): RoomMember | undefined { + if (!id) return undefined; + const parts = id.split(":"); if (parts.length < 2) { logger.warn(