This commit is contained in:
lukasIO
2024-01-09 15:05:20 +01:00
commit eae180722e
40 changed files with 7568 additions and 0 deletions

6
src/pages/_app.tsx Normal file
View File

@@ -0,0 +1,6 @@
import "@/styles/globals.css";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

13
src/pages/_document.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

78
src/pages/api/token.ts Normal file
View File

@@ -0,0 +1,78 @@
import { NextApiRequest, NextApiResponse } from "next";
import { AccessToken } from "livekit-server-sdk";
import type { AccessTokenOptions, VideoGrant } from "livekit-server-sdk";
import { TokenResult } from "../../lib/types";
const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;
const createToken = (userInfo: AccessTokenOptions, grant: VideoGrant) => {
const at = new AccessToken(apiKey, apiSecret, userInfo);
at.addGrant(grant);
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 grant: VideoGrant = {
room: roomName,
roomJoin: true,
canPublish: true,
canPublishData: true,
canSubscribe: true,
};
const token = createToken({ identity, name, metadata }, grant);
const result: TokenResult = {
identity,
accessToken: token,
};
res.status(200).json(result);
} catch (e) {
res.statusMessage = (e as Error).message;
res.status(500).end();
}
}

150
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,150 @@
import Head from "next/head";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Inter } from "next/font/google";
import { generateRandomAlphanumeric } from "@/lib/util";
import { motion, AnimatePresence } from "framer-motion";
import {
LiveKitRoom,
RoomAudioRenderer,
useToken,
} from "@livekit/components-react";
import Playground, {
PlaygroundOutputs,
} from "@/components/playground/Playground";
import { useAppConfig } from "@/hooks/useAppConfig";
import { PlaygroundConnect } from "@/components/PlaygroundConnect";
import { PlaygroundToast, ToastType } from "@/components/toast/PlaygroundToast";
const themeColors = [
"cyan",
"green",
"amber",
"blue",
"violet",
"rose",
"pink",
"teal",
];
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
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 [roomName, setRoomName] = useState(
[generateRandomAlphanumeric(4), generateRandomAlphanumeric(4)].join("-")
);
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(
[generateRandomAlphanumeric(4), generateRandomAlphanumeric(4)].join("-")
);
}
}, [shouldConnect]);
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 handleConnect = useCallback(
(connect: boolean, opts?: { url: string; token: string }) => {
if (connect && opts) {
setLiveKitUrl(opts.url);
setCustomToken(opts.token);
}
setShouldConnect(connect);
},
[]
);
return (
<>
<Head>
<title>Agent Playground</title>
<meta name="description" content="Generated by create next app" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="relative flex flex-col justify-center px-4 items-center h-full w-full bg-black repeating-square-background">
<AnimatePresence>
{toastMessage && (
<motion.div
className="left-0 right-0 top-0 absolute z-10"
initial={{ opacity: 0, translateY: -50 }}
animate={{ opacity: 1, translateY: 0 }}
exit={{ opacity: 0, translateY: -50 }}
>
<PlaygroundToast
message={toastMessage.message}
type={toastMessage.type}
onDismiss={() => {
setToastMessage(null);
}}
/>
</motion.div>
)}
</AnimatePresence>
{liveKitUrl ? (
<LiveKitRoom
className="flex flex-col h-full w-full"
serverUrl={liveKitUrl}
token={customToken ?? token}
audio={appConfig?.inputs.mic}
video={appConfig?.inputs.camera}
connect={shouldConnect}
onError={(e) => {
setToastMessage({ message: e.message, type: "error" });
console.error(e);
}}
>
<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={[{ name: "Room Name", value: roomName }]}
/>
<RoomAudioRenderer />
</LiveKitRoom>
) : (
<PlaygroundConnect
accentColor={themeColors[0]}
onConnectClicked={(url, token) => {
handleConnect(true, { url, token });
}}
/>
)}
</main>
</>
);
}