diff --git a/pages/api/record/start.ts b/app/api/record/start/route.ts
similarity index 72%
rename from pages/api/record/start.ts
rename to app/api/record/start/route.ts
index 50aebdc..a07f381 100644
--- a/pages/api/record/start.ts
+++ b/app/api/record/start/route.ts
@@ -1,9 +1,11 @@
import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk';
-import { NextApiRequest, NextApiResponse } from 'next';
+import { NextResponse } from 'next/server';
-export default async function startRecording(req: NextApiRequest, res: NextApiResponse) {
+export async function GET(req: Request) {
try {
- const { roomName } = req.query;
+ const url = new URL(req.url);
+ const searchParams = url.searchParams;
+ const roomName = searchParams.get('roomName');
/**
* CAUTION:
@@ -13,9 +15,7 @@ export default async function startRecording(req: NextApiRequest, res: NextApiRe
*/
if (typeof roomName !== 'string') {
- res.statusMessage = 'Missing roomName parameter';
- res.status(403).end();
- return;
+ return new NextResponse('Missing roomName parameter', { status: 403 });
}
const {
@@ -36,8 +36,7 @@ export default async function startRecording(req: NextApiRequest, res: NextApiRe
const existingEgresses = await egressClient.listEgress({ roomName });
if (existingEgresses.length > 0 && existingEgresses.some((e) => e.status < 2)) {
- (res.statusMessage = 'Meeting is already being recorded'), res.status(409).end();
- return;
+ return new NextResponse('Meeting is already being recorded', { status: 409 });
}
const fileOutput = new EncodedFileOutput({
@@ -64,13 +63,10 @@ export default async function startRecording(req: NextApiRequest, res: NextApiRe
},
);
- res.status(200).end();
- } catch (e) {
- if (e instanceof Error) {
- res.statusMessage = e.name;
- console.error(e);
- res.status(500).end();
- return;
+ return new NextResponse(null, { status: 200 });
+ } catch (error) {
+ if (error instanceof Error) {
+ return new NextResponse(error.message, { status: 500 });
}
}
}
diff --git a/pages/api/record/stop.ts b/app/api/record/stop/route.ts
similarity index 61%
rename from pages/api/record/stop.ts
rename to app/api/record/stop/route.ts
index cabe8a1..bfbfc49 100644
--- a/pages/api/record/stop.ts
+++ b/app/api/record/stop/route.ts
@@ -1,9 +1,11 @@
import { EgressClient } from 'livekit-server-sdk';
-import { NextApiRequest, NextApiResponse } from 'next';
+import { NextResponse } from 'next/server';
-export default async function stopRecording(req: NextApiRequest, res: NextApiResponse) {
+export async function GET(req: Request) {
try {
- const { roomName } = req.query;
+ const url = new URL(req.url);
+ const searchParams = url.searchParams;
+ const roomName = searchParams.get('roomName');
/**
* CAUTION:
@@ -13,9 +15,7 @@ export default async function stopRecording(req: NextApiRequest, res: NextApiRes
*/
if (typeof roomName !== 'string') {
- res.statusMessage = 'Missing roomName parameter';
- res.status(403).end();
- return;
+ return new NextResponse('Missing roomName parameter', { status: 403 });
}
const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } = process.env;
@@ -28,19 +28,14 @@ export default async function stopRecording(req: NextApiRequest, res: NextApiRes
(info) => info.status < 2,
);
if (activeEgresses.length === 0) {
- res.statusMessage = 'No active recording found';
- res.status(404).end();
- return;
+ return new NextResponse('No active recording found', { status: 404 });
}
await Promise.all(activeEgresses.map((info) => egressClient.stopEgress(info.egressId)));
- res.status(200).end();
- } catch (e) {
- if (e instanceof Error) {
- res.statusMessage = e.name;
- console.error(e);
- res.status(500).end();
- return;
+ return new NextResponse(null, { status: 200 });
+ } catch (error) {
+ if (error instanceof Error) {
+ return new NextResponse(error.message, { status: 500 });
}
}
}
diff --git a/pages/api/token.ts b/app/api/token/route.ts
similarity index 54%
rename from pages/api/token.ts
rename to app/api/token/route.ts
index 321c6b9..3503839 100644
--- a/pages/api/token.ts
+++ b/app/api/token/route.ts
@@ -1,8 +1,7 @@
-import { NextApiRequest, NextApiResponse } from 'next';
-
import { AccessToken } from 'livekit-server-sdk';
import type { AccessTokenOptions, VideoGrant } from 'livekit-server-sdk';
-import { TokenResult } from '../../lib/types';
+import { TokenResult } from '@/lib/types';
+import { NextResponse } from 'next/server';
const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;
@@ -16,35 +15,35 @@ const createToken = (userInfo: AccessTokenOptions, grant: VideoGrant) => {
const roomPattern = /\w{4}\-\w{4}/;
-export default async function handleToken(req: NextApiRequest, res: NextApiResponse) {
+export async function GET(req: Request) {
try {
- const { roomName, identity, name, metadata } = req.query;
+ const url = new URL(req.url);
+ const searchParams = url.searchParams;
+ const roomName = searchParams.get('roomName');
+ const identity = searchParams.get('identity');
+ const name = searchParams.get('name');
+ const metadata = searchParams.get('metadata') ?? '';
if (typeof identity !== 'string' || typeof roomName !== 'string') {
- res.status(403).end();
- return;
+ return new NextResponse('Forbidden', { status: 401 });
+ }
+ if (name === null) {
+ return new NextResponse('Provide a name.', { status: 400 });
}
-
if (Array.isArray(name)) {
- throw Error('provide max one name');
+ return new NextResponse('Provide only one room name.', { status: 400 });
}
if (Array.isArray(metadata)) {
- throw Error('provide max one metadata string');
+ return new NextResponse('Provide only one metadata string.', { status: 400 });
}
// 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.status(400).end();
- return;
+ return new NextResponse('Invalid room name format.', { status: 400 });
}
- // if (!userSession.isAuthenticated) {
- // res.status(403).end();
- // return;
- // }
-
const grant: VideoGrant = {
room: roomName,
roomJoin: true,
@@ -59,9 +58,10 @@ export default async function handleToken(req: NextApiRequest, res: NextApiRespo
accessToken: token,
};
- res.status(200).json(result);
- } catch (e) {
- res.statusMessage = (e as Error).message;
- res.status(500).end();
+ return NextResponse.json(result);
+ } catch (error) {
+ if (error instanceof Error) {
+ return new NextResponse(error.message, { status: 500 });
+ }
}
}
diff --git a/app/api/url/route.ts b/app/api/url/route.ts
new file mode 100644
index 0000000..c6c9426
--- /dev/null
+++ b/app/api/url/route.ts
@@ -0,0 +1,20 @@
+import { getLiveKitURL } from '../../../lib/server-utils';
+import { NextResponse } from 'next/server';
+
+export async function GET(req: Request) {
+ try {
+ const url = new URL(req.url);
+ const searchParams = url.searchParams;
+ const region = searchParams.get('region');
+
+ if (Array.isArray(region)) {
+ throw Error('provide max one region string');
+ }
+ const livekitUrl = getLiveKitURL(region);
+ return NextResponse.json({ url: livekitUrl });
+ } catch (error) {
+ if (error instanceof Error) {
+ return new NextResponse(error.message, { status: 500 });
+ }
+ }
+}
diff --git a/pages/custom/index.tsx b/app/custom/VideoConferenceClientImpl.tsx
similarity index 54%
rename from pages/custom/index.tsx
rename to app/custom/VideoConferenceClientImpl.tsx
index 5f1ee05..02b779c 100644
--- a/pages/custom/index.tsx
+++ b/app/custom/VideoConferenceClientImpl.tsx
@@ -1,4 +1,5 @@
'use client';
+
import { formatChatMessageLinks, LiveKitRoom, VideoConference } from '@livekit/components-react';
import {
ExternalE2EEKeyProvider,
@@ -6,33 +7,33 @@ import {
Room,
RoomConnectOptions,
RoomOptions,
- VideoCodec,
VideoPresets,
+ type VideoCodec,
} from 'livekit-client';
-import { useRouter } from 'next/router';
+import { DebugMode } from '@/lib/Debug';
import { useMemo } from 'react';
-import { decodePassphrase } from '../../lib/client-utils';
-import { DebugMode } from '../../lib/Debug';
+import { decodePassphrase } from '@/lib/client-utils';
+import { SettingsMenu } from '@/lib/SettingsMenu';
-export default function CustomRoomConnection() {
- const router = useRouter();
- const { liveKitUrl, token, codec } = router.query;
-
- const e2eePassphrase =
- typeof window !== 'undefined' && decodePassphrase(window.location.hash.substring(1));
+export function VideoConferenceClientImpl(props: {
+ liveKitUrl: string;
+ token: string;
+ codec: VideoCodec | undefined;
+}) {
const worker =
typeof window !== 'undefined' &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const keyProvider = new ExternalE2EEKeyProvider();
+ const e2eePassphrase =
+ typeof window !== 'undefined' ? decodePassphrase(window.location.hash.substring(1)) : undefined;
const e2eeEnabled = !!(e2eePassphrase && worker);
-
const roomOptions = useMemo((): RoomOptions => {
return {
publishDefaults: {
videoSimulcastLayers: [VideoPresets.h540, VideoPresets.h216],
red: !e2eeEnabled,
- videoCodec: codec as VideoCodec | undefined,
+ videoCodec: props.codec,
},
adaptiveStream: { pixelDensity: 'screen' },
dynacast: true,
@@ -56,28 +57,22 @@ export default function CustomRoomConnection() {
};
}, []);
- if (typeof liveKitUrl !== 'string') {
- return
Missing LiveKit URL
;
- }
- if (typeof token !== 'string') {
- return Missing LiveKit token
;
- }
-
return (
-
- {liveKitUrl && (
-
-
-
-
- )}
-
+
+
+
+
);
}
diff --git a/app/custom/page.tsx b/app/custom/page.tsx
new file mode 100644
index 0000000..c7d5a78
--- /dev/null
+++ b/app/custom/page.tsx
@@ -0,0 +1,28 @@
+import { videoCodecs } from 'livekit-client';
+import { VideoConferenceClientImpl } from './VideoConferenceClientImpl';
+import { isVideoCodec } from '@/lib/types';
+
+export default function CustomRoomConnection(props: {
+ searchParams: {
+ liveKitUrl?: string;
+ token?: string;
+ codec?: string;
+ };
+}) {
+ const { liveKitUrl, token, codec } = props.searchParams;
+ if (typeof liveKitUrl !== 'string') {
+ return Missing LiveKit URL
;
+ }
+ if (typeof token !== 'string') {
+ return Missing LiveKit token
;
+ }
+ if (codec !== undefined && !isVideoCodec(codec)) {
+ return Invalid codec, if defined it has to be [{videoCodecs.join(', ')}].
;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..bf35374
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,56 @@
+import '../styles/globals.css';
+import '@livekit/components-styles';
+import '@livekit/components-styles/prefabs';
+import type { Metadata, Viewport } from 'next';
+
+export const metadata: Metadata = {
+ title: {
+ default: 'LiveKit Meet | Conference app build with LiveKit open source',
+ template: '%s',
+ },
+ description:
+ 'LiveKit is an open source WebRTC project that gives you everything needed to build scalable and real-time audio and/or video experiences in your applications.',
+ twitter: {
+ creator: '@livekitted',
+ site: '@livekitted',
+ card: 'summary_large_image',
+ },
+ openGraph: {
+ url: 'https://meet.livekit.io',
+ images: [
+ {
+ url: 'https://meet.livekit.io/images/livekit-meet-open-graph.png',
+ width: 2000,
+ height: 1000,
+ type: 'image/png',
+ },
+ ],
+ siteName: 'LiveKit Meet',
+ },
+ icons: {
+ icon: {
+ rel: 'icon',
+ url: '/favicon.ico',
+ },
+ apple: [
+ {
+ rel: 'apple-touch-icon',
+ url: '/images/livekit-apple-touch.png',
+ sizes: '180x180',
+ },
+ { rel: 'mask-icon', url: '/images/livekit-safari-pinned-tab.svg', color: '#070707' },
+ ],
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: '#070707',
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/pages/index.tsx b/app/page.tsx
similarity index 80%
rename from pages/index.tsx
rename to app/page.tsx
index 0f8ca17..d23d536 100644
--- a/pages/index.tsx
+++ b/app/page.tsx
@@ -1,19 +1,18 @@
-import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
-import { useRouter } from 'next/router';
-import React, { ReactElement, useState } from 'react';
-import { encodePassphrase, generateRoomId, randomString } from '../lib/client-utils';
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import React, { Suspense, useState } from 'react';
+import { encodePassphrase, generateRoomId, randomString } from '@/lib/client-utils';
import styles from '../styles/Home.module.css';
-interface TabsProps {
- children: ReactElement[];
- selectedIndex?: number;
- onTabSelected?: (index: number) => void;
-}
+function Tabs(props: React.PropsWithChildren<{}>) {
+ const searchParams = useSearchParams();
+ const tabIndex = searchParams?.get('tab') === 'custom' ? 1 : 0;
-function Tabs(props: TabsProps) {
- const activeIndex = props.selectedIndex ?? 0;
- if (!props.children) {
- return <>>;
+ const router = useRouter();
+ function onTabSelected(index: number) {
+ const tab = index === 1 ? 'custom' : 'demo';
+ router.push(`/?tab=${tab}`);
}
let tabs = React.Children.map(props.children, (child, index) => {
@@ -21,23 +20,28 @@ function Tabs(props: TabsProps) {
);
});
+
return (
{tabs}
- {props.children[activeIndex]}
+ {/* @ts-ignore */}
+ {props.children[tabIndex]}
);
}
-function DemoMeetingTab({ label }: { label: string }) {
+function DemoMeetingTab(props: { label: string }) {
const router = useRouter();
const [e2ee, setE2ee] = useState(false);
const [sharedPassphrase, setSharedPassphrase] = useState(randomString(64));
@@ -80,7 +84,7 @@ function DemoMeetingTab({ label }: { label: string }) {
);
}
-function CustomConnectionTab({ label }: { label: string }) {
+function CustomConnectionTab(props: { label: string }) {
const router = useRouter();
const [e2ee, setE2ee] = useState(false);
@@ -156,21 +160,7 @@ function CustomConnectionTab({ label }: { label: string }) {
);
}
-export const getServerSideProps: GetServerSideProps<{ tabIndex: number }> = async ({
- query,
- res,
-}) => {
- res.setHeader('Cache-Control', 'public, max-age=7200');
- const tabIndex = query.tab === 'custom' ? 1 : 0;
- return { props: { tabIndex } };
-};
-
-const Home = ({ tabIndex }: InferGetServerSidePropsType) => {
- const router = useRouter();
- function onTabSelected(index: number) {
- const tab = index === 1 ? 'custom' : 'demo';
- router.push({ query: { tab } });
- }
+export default function Page() {
return (
<>
@@ -188,10 +178,12 @@ const Home = ({ tabIndex }: InferGetServerSidePropsType
-
-
-
-
+
+
+
+
+
+