Allowing settings to be changed in URL hash (#45)
Co-authored-by: mattherzog <Herzog.Matt@gmail.com> Co-authored-by: Neil Dwyer <neildwyer1991@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { generateRandomAlphanumeric } from "@/lib/util";
|
||||
|
||||
import { AccessToken } from "livekit-server-sdk";
|
||||
import type { AccessTokenOptions, VideoGrant } from "livekit-server-sdk";
|
||||
@@ -13,48 +14,19 @@ const createToken = (userInfo: AccessTokenOptions, grant: VideoGrant) => {
|
||||
return at.toJwt();
|
||||
};
|
||||
|
||||
const roomPattern = /\w{4}\-\w{4}/;
|
||||
|
||||
export default async function handleToken(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const { roomName, identity, name, metadata } = req.query;
|
||||
|
||||
if (typeof identity !== "string" || typeof roomName !== "string") {
|
||||
res.statusMessage =
|
||||
"identity and roomName have to be specified in the request";
|
||||
res.status(403).end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey || !apiSecret) {
|
||||
res.statusMessage = "Environment variables aren't set up correctly";
|
||||
res.status(500).end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(name)) {
|
||||
throw Error("provide max one name");
|
||||
}
|
||||
if (Array.isArray(metadata)) {
|
||||
throw Error("provide max one metadata string");
|
||||
}
|
||||
|
||||
// enforce room name to be xxxx-xxxx
|
||||
// this is simple & naive way to prevent user from guessing room names
|
||||
// please use your own authentication mechanisms in your own app
|
||||
if (!roomName.match(roomPattern)) {
|
||||
res.statusMessage = "Invalid roomName";
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!userSession.isAuthenticated) {
|
||||
// res.status(403).end();
|
||||
// return;
|
||||
// }
|
||||
const roomName = `room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`;
|
||||
const identity = `identity-${generateRandomAlphanumeric(4)}`
|
||||
|
||||
const grant: VideoGrant = {
|
||||
room: roomName,
|
||||
@@ -64,7 +36,7 @@ export default async function handleToken(
|
||||
canSubscribe: true,
|
||||
};
|
||||
|
||||
const token = await createToken({ identity, name, metadata }, grant);
|
||||
const token = await createToken({ identity }, grant);
|
||||
const result: TokenResult = {
|
||||
identity,
|
||||
accessToken: token,
|
||||
@@ -75,4 +47,4 @@ export default async function handleToken(
|
||||
res.statusMessage = (e as Error).message;
|
||||
res.status(500).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
import { generateRandomAlphanumeric } from "@/lib/util";
|
||||
import {
|
||||
LiveKitRoom,
|
||||
RoomAudioRenderer,
|
||||
StartAudio,
|
||||
useToken,
|
||||
} from "@livekit/components-react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Inter } from "next/font/google";
|
||||
import Head from "next/head";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { PlaygroundConnect } from "@/components/PlaygroundConnect";
|
||||
import Playground, {
|
||||
PlaygroundMeta,
|
||||
PlaygroundOutputs,
|
||||
} from "@/components/playground/Playground";
|
||||
import Playground, { PlaygroundMeta } from "@/components/playground/Playground";
|
||||
import { PlaygroundToast, ToastType } from "@/components/toast/PlaygroundToast";
|
||||
import { useAppConfig } from "@/hooks/useAppConfig";
|
||||
import { ConfigProvider, useConfig } from "@/hooks/useConfig";
|
||||
import { Mode, TokenGeneratorProvider, useTokenGenerator } from "@/hooks/useTokenGenerator";
|
||||
import { useRef } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const themeColors = [
|
||||
"cyan",
|
||||
@@ -32,77 +30,47 @@ const themeColors = [
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<TokenGeneratorProvider>
|
||||
<HomeInner />
|
||||
</TokenGeneratorProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeInner() {
|
||||
const [toastMessage, setToastMessage] = useState<{
|
||||
message: string;
|
||||
type: ToastType;
|
||||
} | null>(null);
|
||||
const [shouldConnect, setShouldConnect] = useState(false);
|
||||
const [liveKitUrl, setLiveKitUrl] = useState(
|
||||
process.env.NEXT_PUBLIC_LIVEKIT_URL
|
||||
);
|
||||
const [customToken, setCustomToken] = useState<string>();
|
||||
const [metadata, setMetadata] = useState<PlaygroundMeta[]>([]);
|
||||
const { shouldConnect, wsUrl, token, connect, disconnect } =
|
||||
useTokenGenerator();
|
||||
|
||||
const [roomName, setRoomName] = useState(createRoomName());
|
||||
|
||||
const tokenOptions = useMemo(() => {
|
||||
return {
|
||||
userInfo: { identity: generateRandomAlphanumeric(16) },
|
||||
};
|
||||
}, []);
|
||||
|
||||
// set a new room name each time the user disconnects so that a new token gets fetched behind the scenes for a different room
|
||||
useEffect(() => {
|
||||
if (shouldConnect === false) {
|
||||
setRoomName(createRoomName());
|
||||
}
|
||||
}, [shouldConnect]);
|
||||
|
||||
useEffect(() => {
|
||||
const md: PlaygroundMeta[] = [];
|
||||
if (liveKitUrl && liveKitUrl !== process.env.NEXT_PUBLIC_LIVEKIT_URL) {
|
||||
md.push({ name: "LiveKit URL", value: liveKitUrl });
|
||||
}
|
||||
if (!customToken && tokenOptions.userInfo?.identity) {
|
||||
md.push({ name: "Room Name", value: roomName });
|
||||
md.push({
|
||||
name: "Participant Identity",
|
||||
value: tokenOptions.userInfo.identity,
|
||||
});
|
||||
}
|
||||
setMetadata(md);
|
||||
}, [liveKitUrl, roomName, tokenOptions, customToken]);
|
||||
|
||||
const token = useToken("/api/token", roomName, tokenOptions);
|
||||
const appConfig = useAppConfig();
|
||||
const outputs = [
|
||||
appConfig?.outputs.audio && PlaygroundOutputs.Audio,
|
||||
appConfig?.outputs.video && PlaygroundOutputs.Video,
|
||||
appConfig?.outputs.chat && PlaygroundOutputs.Chat,
|
||||
].filter((item) => typeof item !== "boolean") as PlaygroundOutputs[];
|
||||
const {config} = useConfig();
|
||||
|
||||
const handleConnect = useCallback(
|
||||
(connect: boolean, opts?: { url: string; token: string }) => {
|
||||
if (connect && opts) {
|
||||
setLiveKitUrl(opts.url);
|
||||
setCustomToken(opts.token);
|
||||
}
|
||||
setShouldConnect(connect);
|
||||
(c: boolean, mode: Mode) => {
|
||||
c ? connect(mode) : disconnect();
|
||||
},
|
||||
[]
|
||||
[connect, disconnect]
|
||||
);
|
||||
|
||||
const showPG = useMemo(() => {
|
||||
if (process.env.NEXT_PUBLIC_LIVEKIT_URL) {
|
||||
return true;
|
||||
}
|
||||
if(wsUrl) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [wsUrl])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{appConfig?.title ?? "LiveKit Agents Playground"}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={
|
||||
appConfig?.description ??
|
||||
"Quickly prototype and test your multimodal agents"
|
||||
}
|
||||
/>
|
||||
<title>{config.title}</title>
|
||||
<meta name="description" content={config.description} />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
@@ -136,13 +104,11 @@ export default function Home() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{liveKitUrl ? (
|
||||
{showPG ? (
|
||||
<LiveKitRoom
|
||||
className="flex flex-col h-full w-full"
|
||||
serverUrl={liveKitUrl}
|
||||
token={customToken ?? token}
|
||||
audio={appConfig?.inputs.mic}
|
||||
video={appConfig?.inputs.camera}
|
||||
serverUrl={wsUrl}
|
||||
token={token}
|
||||
connect={shouldConnect}
|
||||
onError={(e) => {
|
||||
setToastMessage({ message: e.message, type: "error" });
|
||||
@@ -150,16 +116,13 @@ export default function Home() {
|
||||
}}
|
||||
>
|
||||
<Playground
|
||||
title={appConfig?.title}
|
||||
githubLink={appConfig?.github_link}
|
||||
outputs={outputs}
|
||||
showQR={appConfig?.show_qr}
|
||||
description={appConfig?.description}
|
||||
themeColors={themeColors}
|
||||
defaultColor={appConfig?.theme_color ?? "cyan"}
|
||||
onConnect={handleConnect}
|
||||
metadata={metadata}
|
||||
videoFit={appConfig?.video_fit ?? "cover"}
|
||||
onConnect={(c) => {
|
||||
const mode = process.env.NEXT_PUBLIC_LIVEKIT_URL
|
||||
? "env"
|
||||
: "manual";
|
||||
handleConnect(c, mode);
|
||||
}}
|
||||
/>
|
||||
<RoomAudioRenderer />
|
||||
<StartAudio label="Click to enable audio playback" />
|
||||
@@ -167,18 +130,13 @@ export default function Home() {
|
||||
) : (
|
||||
<PlaygroundConnect
|
||||
accentColor={themeColors[0]}
|
||||
onConnectClicked={(url, token) => {
|
||||
handleConnect(true, { url, token });
|
||||
onConnectClicked={() => {
|
||||
const mode = process.env.NEXT_PUBLIC_LIVEKIT_URL ? "env" : "manual";
|
||||
handleConnect(true, mode);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function createRoomName() {
|
||||
return [generateRandomAlphanumeric(4), generateRandomAlphanumeric(4)].join(
|
||||
"-"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user