Manually disconnect & reconnect the livekit call if our focus changes
Without breaking the 'disconnected' screen
This commit is contained in:
@@ -27,6 +27,13 @@ export interface SFUConfig {
|
|||||||
jwt: string;
|
jwt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sfuConfigEquals(a?: SFUConfig, b?: SFUConfig): boolean {
|
||||||
|
if (a === undefined && b === undefined) return true;
|
||||||
|
if (a === undefined || b === undefined) return false;
|
||||||
|
|
||||||
|
return a.jwt === b.jwt && a.url === b.url;
|
||||||
|
}
|
||||||
|
|
||||||
// The bits we need from MatrixClient
|
// The bits we need from MatrixClient
|
||||||
export type OpenIDClientParts = Pick<
|
export type OpenIDClientParts = Pick<
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
|
|||||||
90
src/livekit/useECConnectionState.ts
Normal file
90
src/livekit/useECConnectionState.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RoomOptions,
|
RoomOptions,
|
||||||
setLogLevel,
|
setLogLevel,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { useConnectionState, useLiveKitRoom } from "@livekit/components-react";
|
import { useLiveKitRoom } from "@livekit/components-react";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -35,6 +35,10 @@ import {
|
|||||||
MediaDevices,
|
MediaDevices,
|
||||||
useMediaDevices,
|
useMediaDevices,
|
||||||
} from "./MediaDevicesContext";
|
} from "./MediaDevicesContext";
|
||||||
|
import {
|
||||||
|
ECConnectionState,
|
||||||
|
useECConnectionState,
|
||||||
|
} from "./useECConnectionState";
|
||||||
|
|
||||||
export type E2EEConfig = {
|
export type E2EEConfig = {
|
||||||
sharedKey: string;
|
sharedKey: string;
|
||||||
@@ -42,11 +46,16 @@ export type E2EEConfig = {
|
|||||||
|
|
||||||
setLogLevel("debug");
|
setLogLevel("debug");
|
||||||
|
|
||||||
|
interface UseLivekitResult {
|
||||||
|
livekitRoom?: Room;
|
||||||
|
connState: ECConnectionState;
|
||||||
|
}
|
||||||
|
|
||||||
export function useLiveKit(
|
export function useLiveKit(
|
||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
sfuConfig?: SFUConfig,
|
sfuConfig?: SFUConfig,
|
||||||
e2eeConfig?: E2EEConfig
|
e2eeConfig?: E2EEConfig
|
||||||
): Room | undefined {
|
): UseLivekitResult {
|
||||||
const e2eeOptions = useMemo(() => {
|
const e2eeOptions = useMemo(() => {
|
||||||
if (!e2eeConfig?.sharedKey) return undefined;
|
if (!e2eeConfig?.sharedKey) return undefined;
|
||||||
|
|
||||||
@@ -101,7 +110,7 @@ export function useLiveKit(
|
|||||||
room: roomWithoutProps,
|
room: roomWithoutProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connectionState = useConnectionState(roomWithoutProps);
|
const connectionState = useECConnectionState(room, sfuConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Sync the requested mute states with LiveKit's mute states. We do it this
|
// Sync the requested mute states with LiveKit's mute states. We do it this
|
||||||
@@ -149,5 +158,8 @@ export function useLiveKit(
|
|||||||
}
|
}
|
||||||
}, [room, devices, connectionState]);
|
}, [room, devices, connectionState]);
|
||||||
|
|
||||||
return room;
|
return {
|
||||||
|
connState: connectionState,
|
||||||
|
livekitRoom: room,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
} from "@livekit/components-react";
|
} from "@livekit/components-react";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import classNames from "classnames";
|
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 { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
|
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 { OverlayTriggerState } from "@react-stately/overlays";
|
||||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { RoomEventCallbacks } from "livekit-client/dist/src/room/Room";
|
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
|
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
|
||||||
|
|
||||||
@@ -74,12 +73,12 @@ import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
|||||||
import { useFullscreen } from "./useFullscreen";
|
import { useFullscreen } from "./useFullscreen";
|
||||||
import { useLayoutStates } from "../video-grid/Layout";
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
import { E2EELock } from "../E2EELock";
|
import { E2EELock } from "../E2EELock";
|
||||||
import { useEventEmitterThree } from "../useEvents";
|
|
||||||
import { useWakeLock } from "../useWakeLock";
|
import { useWakeLock } from "../useWakeLock";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
|
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
|
||||||
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||||
|
import { ECConnectionState } from "../livekit/useECConnectionState";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -87,13 +86,18 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
|||||||
// For now we can disable screensharing in Safari.
|
// For now we can disable screensharing in Safari.
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
|
export interface ActiveCallProps
|
||||||
|
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
|
||||||
e2eeConfig?: E2EEConfig;
|
e2eeConfig?: E2EEConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveCall(props: ActiveCallProps) {
|
export function ActiveCall(props: ActiveCallProps) {
|
||||||
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
||||||
const livekitRoom = useLiveKit(props.muteStates, sfuConfig, props.e2eeConfig);
|
const { livekitRoom, connState } = useLiveKit(
|
||||||
|
props.muteStates,
|
||||||
|
sfuConfig,
|
||||||
|
props.e2eeConfig
|
||||||
|
);
|
||||||
|
|
||||||
if (!livekitRoom) {
|
if (!livekitRoom) {
|
||||||
return null;
|
return null;
|
||||||
@@ -105,7 +109,7 @@ export function ActiveCall(props: ActiveCallProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={livekitRoom}>
|
<RoomContext.Provider value={livekitRoom}>
|
||||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -119,6 +123,7 @@ export interface InCallViewProps {
|
|||||||
onLeave: (error?: Error) => void;
|
onLeave: (error?: Error) => void;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
|
connState: ECConnectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InCallView({
|
export function InCallView({
|
||||||
@@ -130,11 +135,20 @@ export function InCallView({
|
|||||||
onLeave,
|
onLeave,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
otelGroupCallMembership,
|
otelGroupCallMembership,
|
||||||
|
connState,
|
||||||
}: InCallViewProps) {
|
}: InCallViewProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (connState === ConnectionState.Disconnected) {
|
||||||
|
// annoyingly we don't get the disconnection reason this way,
|
||||||
|
// only by listening for the emitted event
|
||||||
|
onLeave(new Error("Disconnected from call server"));
|
||||||
|
}
|
||||||
|
}, [connState, onLeave]);
|
||||||
|
|
||||||
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
|
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -182,27 +196,10 @@ export function InCallView({
|
|||||||
async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
|
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(() => {
|
const onLeavePress = useCallback(() => {
|
||||||
onLeave();
|
onLeave();
|
||||||
}, [onLeave]);
|
}, [onLeave]);
|
||||||
|
|
||||||
useEventEmitterThree<RoomEvent.Disconnected, RoomEventCallbacks>(
|
|
||||||
livekitRoom,
|
|
||||||
RoomEvent.Disconnected,
|
|
||||||
onDisconnected
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport.send(
|
widget?.api.transport.send(
|
||||||
layout === "freedom"
|
layout === "freedom"
|
||||||
@@ -459,6 +456,8 @@ function findMatrixMember(
|
|||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
id: string
|
id: string
|
||||||
): RoomMember | undefined {
|
): RoomMember | undefined {
|
||||||
|
if (!id) return undefined;
|
||||||
|
|
||||||
const parts = id.split(":");
|
const parts = id.split(":");
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
Reference in New Issue
Block a user