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:
parent
2883de2531
commit
15e58cd797
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
20
app/api/url/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
28
app/custom/page.tsx
Normal 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
56
app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user