Manually disconnect & reconnect the livekit call if our focus changes

Without breaking the 'disconnected' screen
This commit is contained in:
David Baker
2023-08-29 12:44:30 +01:00
parent 992e6aa2a3
commit 4cd274b91e
4 changed files with 135 additions and 27 deletions

View File

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

View File

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

View File

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

View File

@@ -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<InCallViewProps, "livekitRoom"> {
export interface ActiveCallProps
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
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 (
<RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} />
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
</RoomContext.Provider>
);
}
@@ -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<HTMLDivElement | null>(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<RoomEvent.Disconnected, RoomEventCallbacks>(
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(