Merge pull request #1166 from vector-im/dbkr/openid

Support for getting SFU config using OIDC
This commit is contained in:
David Baker 2023-07-04 19:05:21 +01:00 committed by GitHub
commit c9f57cb2a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 51 deletions

View file

@ -6,8 +6,7 @@
}
},
"livekit": {
"server_url": "wss://sfu.call.element.dev",
"jwt_service_url": "https://voip-sip-poc.element.io/lk/jwt_service"
"livekit_service_url": "https://lk-jwt-service.lab.element.dev"
},
"posthog": {
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",

View file

@ -55,10 +55,8 @@ export interface ConfigOptions {
// Describes the LiveKit configuration to be used.
livekit?: {
// The LiveKit server URL to connect to.
server_url: string;
// The link to the service that generates JWT tokens to join LiveKit rooms.
jwt_service_url: string;
// The link to the service that returns a livekit url and token to use it
livekit_service_url: string;
};
/**

View file

@ -0,0 +1,96 @@
/*
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 {
OpenIDClientParts,
SFUConfig,
getSFUConfigWithOpenID,
} from "./openIDSFU";
import { ErrorView, LoadingView } from "../FullScreenView";
interface Props {
client: OpenIDClientParts;
livekitServiceURL: string;
roomName: string;
children: ReactNode;
}
const SFUConfigContext = createContext<SFUConfig>(undefined);
export const useSFUConfig = () => useContext(SFUConfigContext);
export function OpenIDLoader({
client,
livekitServiceURL,
roomName,
children,
}: Props) {
const [state, setState] = useState<
SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed
>({ kind: "loading" });
useEffect(() => {
(async () => {
try {
const result = await getSFUConfigWithOpenID(
client,
livekitServiceURL,
roomName
);
setState({ kind: "loaded", sfuConfig: result });
} catch (e) {
logger.error("Failed to fetch SFU config: ", e);
setState({ kind: "failed", error: e });
}
})();
}, [client, livekitServiceURL, roomName]);
switch (state.kind) {
case "loading":
return <LoadingView />;
case "failed":
return <ErrorView error={state.error} />;
case "loaded":
return (
<SFUConfigContext.Provider value={state.sfuConfig}>
{children}
</SFUConfigContext.Provider>
);
}
}
type SFUConfigLoading = {
kind: "loading";
};
type SFUConfigLoaded = {
kind: "loaded";
sfuConfig: SFUConfig;
};
type SFUConfigFailed = {
kind: "failed";
error: Error;
};

54
src/livekit/openIDSFU.ts Normal file
View file

@ -0,0 +1,54 @@
/*
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 { MatrixClient } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/src/logger";
export interface SFUConfig {
url: string;
jwt: string;
}
// The bits we need from MatrixClient
export type OpenIDClientParts = Pick<
MatrixClient,
"getOpenIdToken" | "getDeviceId"
>;
export async function getSFUConfigWithOpenID(
client: OpenIDClientParts,
livekitServiceURL: string,
roomName: string
): Promise<SFUConfig> {
const openIdToken = await client.getOpenIdToken();
logger.debug("Got openID token", openIdToken);
const res = await fetch(livekitServiceURL + "/sfu/get", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
room: roomName,
openid_token: openIdToken,
device_id: client.getDeviceId(),
}),
});
if (!res.ok) {
throw new Error("SFO Config fetch failed with status code " + res.status);
}
return await res.json();
}

View file

@ -1,8 +1,9 @@
import { Room, RoomOptions } from "livekit-client";
import { useLiveKitRoom, useToken } from "@livekit/components-react";
import { useLiveKitRoom } from "@livekit/components-react";
import { useMemo } from "react";
import { defaultLiveKitOptions } from "./options";
import { SFUConfig } from "./openIDSFU";
export type UserChoices = {
audio?: DeviceChoices;
@ -14,29 +15,10 @@ export type DeviceChoices = {
enabled: boolean;
};
export type LiveKitConfig = {
sfuUrl: string;
jwtUrl: string;
roomName: string;
userDisplayName: string;
userIdentity: string;
};
export function useLiveKit(
userChoices: UserChoices,
config: LiveKitConfig
sfuConfig: SFUConfig
): Room | undefined {
const tokenOptions = useMemo(
() => ({
userInfo: {
name: config.userDisplayName,
identity: config.userIdentity,
},
}),
[config.userDisplayName, config.userIdentity]
);
const token = useToken(config.jwtUrl, config.roomName, tokenOptions);
const roomOptions = useMemo((): RoomOptions => {
const options = defaultLiveKitOptions;
options.videoCaptureDefaults = {
@ -51,8 +33,8 @@ export function useLiveKit(
}, [userChoices.video, userChoices.audio]);
const { room } = useLiveKitRoom({
token,
serverUrl: config.sfuUrl,
token: sfuConfig.jwt,
serverUrl: sfuConfig.url,
audio: userChoices.audio?.enabled ?? false,
video: userChoices.video?.enabled ?? false,
options: roomOptions,

View file

@ -28,13 +28,15 @@ import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
import { MatrixInfo } from "./VideoPreview";
import { ActiveCall } from "./InCallView";
import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { UserChoices } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
import { Config } from "../config/Config";
declare global {
interface Window {
@ -218,21 +220,33 @@ export function GroupCallView({
undefined
);
const lkServiceURL = Config.get().livekit?.livekit_service_url;
if (!lkServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
if (error) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered && userChoices) {
return (
<ActiveCall
groupCall={groupCall}
<OpenIDLoader
client={client}
participants={participants}
onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
matrixInfo={matrixInfo}
userChoices={userChoices}
otelGroupCallMembership={otelGroupCallMembership}
/>
livekitServiceURL={lkServiceURL}
roomName={matrixInfo.roomName}
>
<ActiveCall
client={client}
groupCall={groupCall}
participants={participants}
onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
matrixInfo={matrixInfo}
userChoices={userChoices}
otelGroupCallMembership={otelGroupCallMembership}
/>
</OpenIDLoader>
);
} else if (left) {
// The call ended view is shown for two reasons: prompting guests to create

View file

@ -72,7 +72,6 @@ import { MatrixInfo } from "./VideoPreview";
import { useJoinRule } from "./useJoinRule";
import { ParticipantInfo } from "./useGroupCall";
import { ItemData, TileContent } from "../video-grid/VideoTile";
import { Config } from "../config/Config";
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal } from "../settings/SettingsModal";
@ -84,6 +83,7 @@ import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { useMediaDevices } from "../livekit/useMediaDevices";
import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -91,18 +91,13 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
interface ActiveCallProps extends Omit<Props, "livekitRoom"> {
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
userChoices: UserChoices;
}
export function ActiveCall(props: ActiveCallProps) {
const livekitRoom = useLiveKit(props.userChoices, {
sfuUrl: Config.get().livekit!.server_url,
jwtUrl: `${Config.get().livekit!.jwt_service_url}/token`,
roomName: props.matrixInfo.roomName,
userDisplayName: props.matrixInfo.displayName,
userIdentity: `${props.client.getUserId()}:${props.client.getDeviceId()}`,
});
const sfuConfig = useSFUConfig();
const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
return (
livekitRoom && (
@ -113,7 +108,7 @@ export function ActiveCall(props: ActiveCallProps) {
);
}
interface Props {
export interface InCallViewProps {
client: MatrixClient;
groupCall: GroupCall;
livekitRoom: Room;
@ -135,7 +130,7 @@ export function InCallView({
hideHeader,
matrixInfo,
otelGroupCallMembership,
}: Props) {
}: InCallViewProps) {
const { t } = useTranslation();
usePreventScroll();