Migrate to Next app router (#297)

* Migrate Home Page to App Router

* Update themeColor from layout.tsx

* port room page to app router

* small changes

* port custom page to app router

* port token and url api routes

* port start stop routes

* Refactor error handling in GET function

* delete pages folder

* remove unused function

* remove deprecated field

from docs: @deprecated — will be enabled by default and removed in Next.js 15

* wrap useSearchParams in Suspense

* split up custom page into server and client component

* update imports

* simplify

* Refactor error handling in GET function

* refactor to use props for components

* Refactor video codec validation and handling

* Refactor LiveKitRoom component to handle null liveKitUrl

* refactor: improve video codec validation and handling

* add video codec typeguard

* fix isVideoCodec
This commit is contained in:
Jonas Schell 2024-08-21 14:05:42 +02:00 committed by GitHub
parent 2883de2531
commit 15e58cd797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 288 additions and 307 deletions

View File

@ -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 });
}
}
}

View File

@ -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 });
}
}
}

View File

@ -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 });
}
}
}

20
app/api/url/route.ts Normal file
View File

@ -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 });
}
}
}

View File

@ -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 <h2>Missing LiveKit URL</h2>;
}
if (typeof token !== 'string') {
return <h2>Missing LiveKit token</h2>;
}
return (
<main data-lk-theme="default">
{liveKitUrl && (
<LiveKitRoom
room={room}
token={token}
connectOptions={connectOptions}
serverUrl={liveKitUrl}
audio={true}
video={true}
>
<VideoConference chatMessageFormatter={formatChatMessageLinks} />
<DebugMode logLevel={LogLevel.debug} />
</LiveKitRoom>
)}
</main>
<LiveKitRoom
room={room}
token={props.token}
connectOptions={connectOptions}
serverUrl={props.liveKitUrl}
audio={true}
video={true}
>
<VideoConference
chatMessageFormatter={formatChatMessageLinks}
SettingsComponent={
process.env.NEXT_PUBLIC_SHOW_SETTINGS_MENU === 'true' ? SettingsMenu : undefined
}
/>
<DebugMode logLevel={LogLevel.debug} />
</LiveKitRoom>
);
}

28
app/custom/page.tsx Normal file
View File

@ -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 <h2>Missing LiveKit URL</h2>;
}
if (typeof token !== 'string') {
return <h2>Missing LiveKit token</h2>;
}
if (codec !== undefined && !isVideoCodec(codec)) {
return <h2>Invalid codec, if defined it has to be [{videoCodecs.join(', ')}].</h2>;
}
return (
<main data-lk-theme="default">
<VideoConferenceClientImpl liveKitUrl={liveKitUrl} token={token} codec={codec} />
</main>
);
}

56
app/layout.tsx Normal file
View File

@ -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 (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@ -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) {
<button
className="lk-button"
onClick={() => {
if (props.onTabSelected) props.onTabSelected(index);
if (onTabSelected) {
onTabSelected(index);
}
}}
aria-pressed={activeIndex === index}
aria-pressed={tabIndex === index}
>
{/* @ts-ignore */}
{child?.props.label}
</button>
);
});
return (
<div className={styles.tabContainer}>
<div className={styles.tabSelect}>{tabs}</div>
{props.children[activeIndex]}
{/* @ts-ignore */}
{props.children[tabIndex]}
</div>
);
}
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<typeof getServerSideProps>) => {
const router = useRouter();
function onTabSelected(index: number) {
const tab = index === 1 ? 'custom' : 'demo';
router.push({ query: { tab } });
}
export default function Page() {
return (
<>
<main className={styles.main} data-lk-theme="default">
@ -188,10 +178,12 @@ const Home = ({ tabIndex }: InferGetServerSidePropsType<typeof getServerSideProp
and Next.js.
</h2>
</div>
<Tabs selectedIndex={tabIndex} onTabSelected={onTabSelected}>
<DemoMeetingTab label="Demo" />
<CustomConnectionTab label="Custom" />
</Tabs>
<Suspense fallback="Loading">
<Tabs>
<DemoMeetingTab label="Demo" />
<CustomConnectionTab label="Custom" />
</Tabs>
</Suspense>
</main>
<footer data-lk-theme="default">
Hosted on{' '}
@ -206,6 +198,4 @@ const Home = ({ tabIndex }: InferGetServerSidePropsType<typeof getServerSideProp
</footer>
</>
);
};
export default Home;
}

View File

@ -1,4 +1,5 @@
'use client';
import {
LiveKitRoom,
VideoConference,
@ -18,23 +19,20 @@ import {
setLogLevel,
} from 'livekit-client';
import type { NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useRouter, useSearchParams } from 'next/navigation';
import * as React from 'react';
import { DebugMode } from '../../lib/Debug';
import { decodePassphrase, useServerUrl } from '../../lib/client-utils';
import { SettingsMenu } from '../../lib/SettingsMenu';
import { RecordingIndicator } from '../../lib/RecordingIndicator';
import { DebugMode } from '@/lib/Debug';
import { decodePassphrase, useServerUrl } from '@/lib/client-utils';
import { SettingsMenu } from '@/lib/SettingsMenu';
import { RecordingIndicator } from '@/lib/RecordingIndicator';
import { isVideoCodec } from '@/lib/types';
const Home: NextPage = () => {
export default function Page({ params }: { params: { roomName: string } }) {
const router = useRouter();
const { name: roomName } = router.query;
const roomName = params.roomName;
const [preJoinChoices, setPreJoinChoices] = React.useState<LocalUserChoices | undefined>(
undefined,
);
const preJoinDefaults = React.useMemo(() => {
return {
username: '',
@ -42,89 +40,71 @@ const Home: NextPage = () => {
audioEnabled: true,
};
}, []);
const handlePreJoinSubmit = React.useCallback((values: LocalUserChoices) => {
setPreJoinChoices(values);
}, []);
const onPreJoinError = React.useCallback((e: any) => {
console.error(e);
}, []);
const onLeave = React.useCallback(() => router.push('/'), []);
return (
<>
<Head>
<title>LiveKit Meet</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main data-lk-theme="default">
{roomName && !Array.isArray(roomName) && preJoinChoices ? (
<ActiveRoom
roomName={roomName}
userChoices={preJoinChoices}
onLeave={onLeave}
></ActiveRoom>
) : (
<div style={{ display: 'grid', placeItems: 'center', height: '100%' }}>
<PreJoin
onError={onPreJoinError}
defaults={preJoinDefaults}
onSubmit={handlePreJoinSubmit}
></PreJoin>
</div>
)}
</main>
</>
<main data-lk-theme="default">
{roomName && !Array.isArray(roomName) && preJoinChoices ? (
<ActiveRoom roomName={roomName} userChoices={preJoinChoices} onLeave={onLeave}></ActiveRoom>
) : (
<div style={{ display: 'grid', placeItems: 'center', height: '100%' }}>
<PreJoin
onError={onPreJoinError}
defaults={preJoinDefaults}
onSubmit={handlePreJoinSubmit}
/>
</div>
)}
</main>
);
};
}
export default Home;
type ActiveRoomProps = {
function ActiveRoom(props: {
userChoices: LocalUserChoices;
roomName: string;
region?: string;
onLeave?: () => void;
};
const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
onLeave: () => void;
}) {
const searchParams = useSearchParams();
const region = searchParams?.get('region');
const hq = searchParams?.get('hq');
const codec = searchParams?.get('codec');
const tokenOptions = React.useMemo(() => {
return {
userInfo: {
identity: userChoices.username,
name: userChoices.username,
identity: props.userChoices.username,
name: props.userChoices.username,
},
};
}, [userChoices.username]);
const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions);
const router = useRouter();
const { region, hq, codec } = router.query;
}, [props.userChoices.username]);
const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, props.roomName, tokenOptions);
const e2eePassphrase =
typeof window !== 'undefined' && decodePassphrase(location.hash.substring(1));
const liveKitUrl = useServerUrl(region as string | undefined);
const liveKitUrl = useServerUrl(typeof region === 'string' ? region : undefined);
const worker =
typeof window !== 'undefined' &&
e2eePassphrase &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const e2eeEnabled = !!(e2eePassphrase && worker);
const keyProvider = new ExternalE2EEKeyProvider();
const roomOptions = React.useMemo((): RoomOptions => {
let videoCodec: VideoCodec | undefined = (
Array.isArray(codec) ? codec[0] : codec ?? 'vp9'
) as VideoCodec;
let videoCodec: VideoCodec | undefined = codec && isVideoCodec(codec) ? codec : 'vp9';
if (e2eeEnabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
videoCodec = undefined;
}
return {
videoCaptureDefaults: {
deviceId: userChoices.videoDeviceId ?? undefined,
deviceId: props.userChoices.videoDeviceId ?? undefined,
resolution: hq === 'true' ? VideoPresets.h2160 : VideoPresets.h720,
},
publishDefaults: {
@ -137,7 +117,7 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
videoCodec,
},
audioCaptureDefaults: {
deviceId: userChoices.audioDeviceId ?? undefined,
deviceId: props.userChoices.audioDeviceId ?? undefined,
},
adaptiveStream: { pixelDensity: 'screen' },
dynacast: true,
@ -150,7 +130,7 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
};
// @ts-ignore
setLogLevel('debug', 'lk-e2ee');
}, [userChoices, hq, codec]);
}, [props.userChoices, hq, codec]);
const room = React.useMemo(() => new Room(roomOptions), []);
@ -171,28 +151,30 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => {
};
}, []);
if (!liveKitUrl) {
return null;
}
return (
<>
{liveKitUrl && (
<LiveKitRoom
room={room}
token={token}
serverUrl={liveKitUrl}
connectOptions={connectOptions}
video={userChoices.videoEnabled}
audio={userChoices.audioEnabled}
onDisconnected={onLeave}
>
<VideoConference
chatMessageFormatter={formatChatMessageLinks}
SettingsComponent={
process.env.NEXT_PUBLIC_SHOW_SETTINGS_MENU === 'true' ? SettingsMenu : undefined
}
/>
<DebugMode />
<RecordingIndicator />
</LiveKitRoom>
)}
<LiveKitRoom
room={room}
token={token}
serverUrl={liveKitUrl}
connectOptions={connectOptions}
video={props.userChoices.videoEnabled}
audio={props.userChoices.audioEnabled}
onDisconnected={props.onLeave}
>
<VideoConference
chatMessageFormatter={formatChatMessageLinks}
SettingsComponent={
process.env.NEXT_PUBLIC_SHOW_SETTINGS_MENU === 'true' ? SettingsMenu : undefined
}
/>
<DebugMode />
<RecordingIndicator />
</LiveKitRoom>
</>
);
};
}

View File

@ -1,13 +1,6 @@
import { RoomServiceClient } from 'livekit-server-sdk';
export function getRoomClient(): RoomServiceClient {
checkKeys();
return new RoomServiceClient(getLiveKitURL());
}
export function getLiveKitURL(region?: string | string[]): string {
export function getLiveKitURL(region: string | null): string {
let targetKey = 'LIVEKIT_URL';
if (region && !Array.isArray(region)) {
if (region) {
targetKey = `LIVEKIT_URL_${region}`.toUpperCase();
}
const url = process.env[targetKey];
@ -16,12 +9,3 @@ export function getLiveKitURL(region?: string | string[]): string {
}
return url;
}
function checkKeys() {
if (typeof process.env.LIVEKIT_API_KEY === 'undefined') {
throw new Error('LIVEKIT_API_KEY is not defined');
}
if (typeof process.env.LIVEKIT_API_SECRET === 'undefined') {
throw new Error('LIVEKIT_API_SECRET is not defined');
}
}

View File

@ -1,4 +1,5 @@
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client';
import { LocalAudioTrack, LocalVideoTrack, videoCodecs } from 'livekit-client';
import { VideoCodec } from 'livekit-client';
export interface SessionProps {
roomName: string;
@ -14,3 +15,7 @@ export interface TokenResult {
identity: string;
accessToken: string;
}
export function isVideoCodec(codec: string): codec is VideoCodec {
return videoCodecs.includes(codec as VideoCodec);
}

View File

@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
swcMinify: false,
productionBrowserSourceMaps: true,
webpack: (config, { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }) => {
// Important: return the modified config

View File

@ -1,60 +0,0 @@
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import '@livekit/components-styles';
import '@livekit/components-styles/prefabs';
import { DefaultSeo } from 'next-seo';
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<DefaultSeo
title="LiveKit Meet | Conference app build with LiveKit Open Source"
titleTemplate="%s"
defaultTitle="LiveKit Meet | Conference app build with LiveKit open source"
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={{
handle: '@livekitted',
site: '@livekitted',
cardType: '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',
},
],
site_name: 'LiveKit Meet',
}}
additionalMetaTags={[
{
property: 'theme-color',
content: '#070707',
},
]}
additionalLinkTags={[
{
rel: 'icon',
href: '/favicon.ico',
},
{
rel: 'apple-touch-icon',
href: '/images/livekit-apple-touch.png',
sizes: '180x180',
},
{
rel: 'mask-icon',
href: '/images/livekit-safari-pinned-tab.svg',
color: '#070707',
},
]}
/>
<Component {...pageProps} />
</>
);
}
export default MyApp;

View File

@ -1,17 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getLiveKitURL } from '../../lib/server-utils';
export default async function handleServerUrl(req: NextApiRequest, res: NextApiResponse) {
try {
const { region } = req.query;
if (Array.isArray(region)) {
throw Error('provide max one region string');
}
const url = getLiveKitURL(region);
res.status(200).json({ url });
} catch (e) {
res.statusMessage = (e as Error).message;
res.status(500).end();
}
}

View File

@ -14,8 +14,16 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"sourceMap": true
"sourceMap": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}