...
This commit is contained in:
89
app/api/connection-details/route.ts
Normal file
89
app/api/connection-details/route.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { randomString } from '@/lib/client-utils';
|
||||
import { getLiveKitURL } from '@/lib/getLiveKitURL';
|
||||
import { ConnectionDetails } from '@/lib/types';
|
||||
import { AccessToken, AccessTokenOptions, VideoGrant } from 'livekit-server-sdk';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_KEY = process.env.LIVEKIT_API_KEY;
|
||||
const API_SECRET = process.env.LIVEKIT_API_SECRET;
|
||||
const LIVEKIT_URL = process.env.LIVEKIT_URL;
|
||||
|
||||
const COOKIE_KEY = 'random-participant-postfix';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Parse query parameters
|
||||
const roomName = request.nextUrl.searchParams.get('roomName');
|
||||
const participantName = request.nextUrl.searchParams.get('participantName');
|
||||
const metadata = request.nextUrl.searchParams.get('metadata') ?? '';
|
||||
const region = request.nextUrl.searchParams.get('region');
|
||||
if (!LIVEKIT_URL) {
|
||||
throw new Error('LIVEKIT_URL is not defined');
|
||||
}
|
||||
const livekitServerUrl = region ? getLiveKitURL(LIVEKIT_URL, region) : LIVEKIT_URL;
|
||||
let randomParticipantPostfix = request.cookies.get(COOKIE_KEY)?.value;
|
||||
if (livekitServerUrl === undefined) {
|
||||
throw new Error('Invalid region');
|
||||
}
|
||||
|
||||
if (typeof roomName !== 'string') {
|
||||
return new NextResponse('Missing required query parameter: roomName', { status: 400 });
|
||||
}
|
||||
if (participantName === null) {
|
||||
return new NextResponse('Missing required query parameter: participantName', { status: 400 });
|
||||
}
|
||||
|
||||
// Generate participant token
|
||||
if (!randomParticipantPostfix) {
|
||||
randomParticipantPostfix = randomString(4);
|
||||
}
|
||||
const participantToken = await createParticipantToken(
|
||||
{
|
||||
identity: `${participantName}__${randomParticipantPostfix}`,
|
||||
name: participantName,
|
||||
metadata,
|
||||
},
|
||||
roomName,
|
||||
);
|
||||
|
||||
// Return connection details
|
||||
const data: ConnectionDetails = {
|
||||
serverUrl: livekitServerUrl,
|
||||
roomName: roomName,
|
||||
participantToken: participantToken,
|
||||
participantName: participantName,
|
||||
};
|
||||
return new NextResponse(JSON.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Set-Cookie': `${COOKIE_KEY}=${randomParticipantPostfix}; Path=/; HttpOnly; SameSite=Strict; Secure; Expires=${getCookieExpirationTime()}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return new NextResponse(error.message, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createParticipantToken(userInfo: AccessTokenOptions, roomName: string) {
|
||||
const at = new AccessToken(API_KEY, API_SECRET, userInfo);
|
||||
at.ttl = '5m';
|
||||
const grant: VideoGrant = {
|
||||
room: roomName,
|
||||
roomJoin: true,
|
||||
canPublish: true,
|
||||
canPublishData: true,
|
||||
canSubscribe: true,
|
||||
};
|
||||
at.addGrant(grant);
|
||||
return at.toJwt();
|
||||
}
|
||||
|
||||
function getCookieExpirationTime(): string {
|
||||
var now = new Date();
|
||||
var time = now.getTime();
|
||||
var expireTime = time + 60 * 120 * 1000;
|
||||
now.setTime(expireTime);
|
||||
return now.toUTCString();
|
||||
}
|
70
app/api/record/start/route.ts
Normal file
70
app/api/record/start/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const roomName = req.nextUrl.searchParams.get('roomName');
|
||||
|
||||
/**
|
||||
* CAUTION:
|
||||
* for simplicity this implementation does not authenticate users and therefore allows anyone with knowledge of a roomName
|
||||
* to start/stop recordings for that room.
|
||||
* DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS
|
||||
*/
|
||||
|
||||
if (roomName === null) {
|
||||
return new NextResponse('Missing roomName parameter', { status: 403 });
|
||||
}
|
||||
|
||||
const {
|
||||
LIVEKIT_API_KEY,
|
||||
LIVEKIT_API_SECRET,
|
||||
LIVEKIT_URL,
|
||||
S3_KEY_ID,
|
||||
S3_KEY_SECRET,
|
||||
S3_BUCKET,
|
||||
S3_ENDPOINT,
|
||||
S3_REGION,
|
||||
} = process.env;
|
||||
|
||||
const hostURL = new URL(LIVEKIT_URL!);
|
||||
hostURL.protocol = 'https:';
|
||||
|
||||
const egressClient = new EgressClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||
|
||||
const existingEgresses = await egressClient.listEgress({ roomName });
|
||||
if (existingEgresses.length > 0 && existingEgresses.some((e) => e.status < 2)) {
|
||||
return new NextResponse('Meeting is already being recorded', { status: 409 });
|
||||
}
|
||||
|
||||
const fileOutput = new EncodedFileOutput({
|
||||
filepath: `${new Date(Date.now()).toISOString()}-${roomName}.mp4`,
|
||||
output: {
|
||||
case: 's3',
|
||||
value: new S3Upload({
|
||||
endpoint: S3_ENDPOINT,
|
||||
accessKey: S3_KEY_ID,
|
||||
secret: S3_KEY_SECRET,
|
||||
region: S3_REGION,
|
||||
bucket: S3_BUCKET,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await egressClient.startRoomCompositeEgress(
|
||||
roomName,
|
||||
{
|
||||
file: fileOutput,
|
||||
},
|
||||
{
|
||||
layout: 'speaker',
|
||||
},
|
||||
);
|
||||
|
||||
return new NextResponse(null, { status: 200 });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return new NextResponse(error.message, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
39
app/api/record/stop/route.ts
Normal file
39
app/api/record/stop/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { EgressClient } from 'livekit-server-sdk';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const roomName = req.nextUrl.searchParams.get('roomName');
|
||||
|
||||
/**
|
||||
* CAUTION:
|
||||
* for simplicity this implementation does not authenticate users and therefore allows anyone with knowledge of a roomName
|
||||
* to start/stop recordings for that room.
|
||||
* DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS
|
||||
*/
|
||||
|
||||
if (roomName === null) {
|
||||
return new NextResponse('Missing roomName parameter', { status: 403 });
|
||||
}
|
||||
|
||||
const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } = process.env;
|
||||
|
||||
const hostURL = new URL(LIVEKIT_URL!);
|
||||
hostURL.protocol = 'https:';
|
||||
|
||||
const egressClient = new EgressClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||
const activeEgresses = (await egressClient.listEgress({ roomName })).filter(
|
||||
(info) => info.status < 2,
|
||||
);
|
||||
if (activeEgresses.length === 0) {
|
||||
return new NextResponse('No active recording found', { status: 404 });
|
||||
}
|
||||
await Promise.all(activeEgresses.map((info) => egressClient.stopEgress(info.egressId)));
|
||||
|
||||
return new NextResponse(null, { status: 200 });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return new NextResponse(error.message, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
96
app/custom/VideoConferenceClientImpl.tsx
Normal file
96
app/custom/VideoConferenceClientImpl.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import { formatChatMessageLinks, RoomContext, VideoConference } from '@livekit/components-react';
|
||||
import {
|
||||
ExternalE2EEKeyProvider,
|
||||
LogLevel,
|
||||
Room,
|
||||
RoomConnectOptions,
|
||||
RoomOptions,
|
||||
VideoPresets,
|
||||
type VideoCodec,
|
||||
} from 'livekit-client';
|
||||
import { DebugMode } from '@/lib/Debug';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { KeyboardShortcuts } from '@/lib/KeyboardShortcuts';
|
||||
import { SettingsMenu } from '@/lib/SettingsMenu';
|
||||
import { useSetupE2EE } from '@/lib/useSetupE2EE';
|
||||
import { useLowCPUOptimizer } from '@/lib/usePerfomanceOptimiser';
|
||||
|
||||
export function VideoConferenceClientImpl(props: {
|
||||
liveKitUrl: string;
|
||||
token: string;
|
||||
codec: VideoCodec | undefined;
|
||||
}) {
|
||||
const keyProvider = new ExternalE2EEKeyProvider();
|
||||
const { worker, e2eePassphrase } = useSetupE2EE();
|
||||
const e2eeEnabled = !!(e2eePassphrase && worker);
|
||||
|
||||
const [e2eeSetupComplete, setE2eeSetupComplete] = useState(false);
|
||||
|
||||
const roomOptions = useMemo((): RoomOptions => {
|
||||
return {
|
||||
publishDefaults: {
|
||||
videoSimulcastLayers: [VideoPresets.h540, VideoPresets.h216],
|
||||
red: !e2eeEnabled,
|
||||
videoCodec: props.codec,
|
||||
},
|
||||
adaptiveStream: { pixelDensity: 'screen' },
|
||||
dynacast: true,
|
||||
e2ee: e2eeEnabled
|
||||
? {
|
||||
keyProvider,
|
||||
worker,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}, [e2eeEnabled, props.codec, keyProvider, worker]);
|
||||
|
||||
const room = useMemo(() => new Room(roomOptions), [roomOptions]);
|
||||
|
||||
const connectOptions = useMemo((): RoomConnectOptions => {
|
||||
return {
|
||||
autoSubscribe: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (e2eeEnabled) {
|
||||
keyProvider.setKey(e2eePassphrase).then(() => {
|
||||
room.setE2EEEnabled(true).then(() => {
|
||||
setE2eeSetupComplete(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setE2eeSetupComplete(true);
|
||||
}
|
||||
}, [e2eeEnabled, e2eePassphrase, keyProvider, room, setE2eeSetupComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (e2eeSetupComplete) {
|
||||
room.connect(props.liveKitUrl, props.token, connectOptions).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
room.localParticipant.enableCameraAndMicrophone().catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}, [room, props.liveKitUrl, props.token, connectOptions, e2eeSetupComplete]);
|
||||
|
||||
useLowCPUOptimizer(room);
|
||||
|
||||
return (
|
||||
<div className="lk-room-container">
|
||||
<RoomContext.Provider value={room}>
|
||||
<KeyboardShortcuts />
|
||||
<VideoConference
|
||||
chatMessageFormatter={formatChatMessageLinks}
|
||||
SettingsComponent={
|
||||
process.env.NEXT_PUBLIC_SHOW_SETTINGS_MENU === 'true' ? SettingsMenu : undefined
|
||||
}
|
||||
/>
|
||||
<DebugMode logLevel={LogLevel.debug} />
|
||||
</RoomContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
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 async function CustomRoomConnection(props: {
|
||||
searchParams: Promise<{
|
||||
liveKitUrl?: string;
|
||||
token?: string;
|
||||
codec?: string;
|
||||
}>;
|
||||
}) {
|
||||
const { liveKitUrl, token, codec } = await 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" style={{ height: '100%' }}>
|
||||
<VideoConferenceClientImpl liveKitUrl={liveKitUrl} token={token} codec={codec} />
|
||||
</main>
|
||||
);
|
||||
}
|
60
app/layout.tsx
Normal file
60
app/layout.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import '../styles/globals.css';
|
||||
import '@livekit/components-styles';
|
||||
import '@livekit/components-styles/prefabs';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'LiveKit Meet | Conference app build with LiveKit open source',
|
||||
template: '%s',
|
||||
},
|
||||
description:
|
||||
'',
|
||||
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 data-lk-theme="default">
|
||||
<Toaster />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
73
app/page.tsx
Normal file
73
app/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import { encodePassphrase, generateRoomId, randomString } from '@/lib/client-utils';
|
||||
import styles from '../styles/Home.module.css';
|
||||
|
||||
function Tabs(props: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
<div className={styles.tabContainer}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DemoMeetingTab(props: { label: string }) {
|
||||
const [e2ee, setE2ee] = useState(false);
|
||||
const [sharedPassphrase, setSharedPassphrase] = useState(randomString(64));
|
||||
const startMeeting = () => {
|
||||
let url = `/rooms/${generateRoomId()}`;
|
||||
if (e2ee) {
|
||||
url += `#${encodePassphrase(sharedPassphrase)}`;
|
||||
}
|
||||
window.location.href = url;
|
||||
};
|
||||
return (
|
||||
<div className={styles.tabContent}>
|
||||
<h1 style={{ textAlign: 'center', marginTop: 0 }}>Hero Meet</h1>
|
||||
<p style={{ margin: 0, textAlign: 'center' }}>
|
||||
Let's get started.
|
||||
</p>
|
||||
<button style={{ marginTop: '1rem' }} className="lk-button" onClick={startMeeting}>
|
||||
Start Meeting
|
||||
</button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem' }}>
|
||||
<input
|
||||
id="use-e2ee"
|
||||
type="checkbox"
|
||||
checked={e2ee}
|
||||
onChange={(ev) => setE2ee(ev.target.checked)}
|
||||
></input>
|
||||
<label htmlFor="use-e2ee">Enable end-to-end encryption</label>
|
||||
</div>
|
||||
{e2ee && (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem' }}>
|
||||
<label htmlFor="passphrase">Passphrase</label>
|
||||
<input
|
||||
id="passphrase"
|
||||
type="password"
|
||||
value={sharedPassphrase}
|
||||
onChange={(ev) => setSharedPassphrase(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<main className={styles.main} data-lk-theme="default">
|
||||
<Suspense fallback="Loading">
|
||||
<Tabs>
|
||||
<DemoMeetingTab label="Demo" />
|
||||
</Tabs>
|
||||
</Suspense>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
232
app/rooms/[roomName]/PageClientImpl.tsx
Normal file
232
app/rooms/[roomName]/PageClientImpl.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { decodePassphrase } from '@/lib/client-utils';
|
||||
import { DebugMode } from '@/lib/Debug';
|
||||
import { KeyboardShortcuts } from '@/lib/KeyboardShortcuts';
|
||||
import { RecordingIndicator } from '@/lib/RecordingIndicator';
|
||||
import { SettingsMenu } from '@/lib/SettingsMenu';
|
||||
import { ConnectionDetails } from '@/lib/types';
|
||||
import {
|
||||
formatChatMessageLinks,
|
||||
LocalUserChoices,
|
||||
PreJoin,
|
||||
RoomContext,
|
||||
VideoConference,
|
||||
} from '@livekit/components-react';
|
||||
import {
|
||||
ExternalE2EEKeyProvider,
|
||||
RoomOptions,
|
||||
VideoCodec,
|
||||
VideoPresets,
|
||||
Room,
|
||||
DeviceUnsupportedError,
|
||||
RoomConnectOptions,
|
||||
RoomEvent,
|
||||
TrackPublishDefaults,
|
||||
VideoCaptureOptions,
|
||||
} from 'livekit-client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSetupE2EE } from '@/lib/useSetupE2EE';
|
||||
import { useLowCPUOptimizer } from '@/lib/usePerfomanceOptimiser';
|
||||
|
||||
const CONN_DETAILS_ENDPOINT =
|
||||
process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT ?? '/api/connection-details';
|
||||
const SHOW_SETTINGS_MENU = process.env.NEXT_PUBLIC_SHOW_SETTINGS_MENU == 'true';
|
||||
|
||||
export function PageClientImpl(props: {
|
||||
roomName: string;
|
||||
region?: string;
|
||||
hq: boolean;
|
||||
codec: VideoCodec;
|
||||
}) {
|
||||
const [preJoinChoices, setPreJoinChoices] = React.useState<LocalUserChoices | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const preJoinDefaults = React.useMemo(() => {
|
||||
return {
|
||||
username: '',
|
||||
videoEnabled: true,
|
||||
audioEnabled: true,
|
||||
};
|
||||
}, []);
|
||||
const [connectionDetails, setConnectionDetails] = React.useState<ConnectionDetails | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const handlePreJoinSubmit = React.useCallback(async (values: LocalUserChoices) => {
|
||||
setPreJoinChoices(values);
|
||||
const url = new URL(CONN_DETAILS_ENDPOINT, window.location.origin);
|
||||
url.searchParams.append('roomName', props.roomName);
|
||||
url.searchParams.append('participantName', values.username);
|
||||
if (props.region) {
|
||||
url.searchParams.append('region', props.region);
|
||||
}
|
||||
const connectionDetailsResp = await fetch(url.toString());
|
||||
const connectionDetailsData = await connectionDetailsResp.json();
|
||||
setConnectionDetails(connectionDetailsData);
|
||||
}, []);
|
||||
const handlePreJoinError = React.useCallback((e: any) => console.error(e), []);
|
||||
|
||||
return (
|
||||
<main data-lk-theme="default" style={{ height: '100%' }}>
|
||||
{connectionDetails === undefined || preJoinChoices === undefined ? (
|
||||
<div style={{ display: 'grid', placeItems: 'center', height: '100%' }}>
|
||||
<PreJoin
|
||||
defaults={preJoinDefaults}
|
||||
onSubmit={handlePreJoinSubmit}
|
||||
onError={handlePreJoinError}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<VideoConferenceComponent
|
||||
connectionDetails={connectionDetails}
|
||||
userChoices={preJoinChoices}
|
||||
options={{ codec: props.codec, hq: props.hq }}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function VideoConferenceComponent(props: {
|
||||
userChoices: LocalUserChoices;
|
||||
connectionDetails: ConnectionDetails;
|
||||
options: {
|
||||
hq: boolean;
|
||||
codec: VideoCodec;
|
||||
};
|
||||
}) {
|
||||
const keyProvider = new ExternalE2EEKeyProvider();
|
||||
const { worker, e2eePassphrase } = useSetupE2EE();
|
||||
const e2eeEnabled = !!(e2eePassphrase && worker);
|
||||
|
||||
const [e2eeSetupComplete, setE2eeSetupComplete] = React.useState(false);
|
||||
|
||||
const roomOptions = React.useMemo((): RoomOptions => {
|
||||
let videoCodec: VideoCodec | undefined = props.options.codec ? props.options.codec : 'vp9';
|
||||
if (e2eeEnabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
|
||||
videoCodec = undefined;
|
||||
}
|
||||
const videoCaptureDefaults: VideoCaptureOptions = {
|
||||
deviceId: props.userChoices.videoDeviceId ?? undefined,
|
||||
resolution: props.options.hq ? VideoPresets.h2160 : VideoPresets.h720,
|
||||
};
|
||||
const publishDefaults: TrackPublishDefaults = {
|
||||
dtx: false,
|
||||
videoSimulcastLayers: props.options.hq
|
||||
? [VideoPresets.h1080, VideoPresets.h720]
|
||||
: [VideoPresets.h540, VideoPresets.h216],
|
||||
red: !e2eeEnabled,
|
||||
videoCodec,
|
||||
};
|
||||
return {
|
||||
videoCaptureDefaults: videoCaptureDefaults,
|
||||
publishDefaults: publishDefaults,
|
||||
audioCaptureDefaults: {
|
||||
deviceId: props.userChoices.audioDeviceId ?? undefined,
|
||||
},
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
e2ee: keyProvider && worker && e2eeEnabled ? { keyProvider, worker } : undefined,
|
||||
};
|
||||
}, [props.userChoices, props.options.hq, props.options.codec]);
|
||||
|
||||
const room = React.useMemo(() => new Room(roomOptions), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (e2eeEnabled) {
|
||||
keyProvider
|
||||
.setKey(decodePassphrase(e2eePassphrase))
|
||||
.then(() => {
|
||||
room.setE2EEEnabled(true).catch((e) => {
|
||||
if (e instanceof DeviceUnsupportedError) {
|
||||
alert(
|
||||
`You're trying to join an encrypted meeting, but your browser does not support it. Please update it to the latest version and try again.`,
|
||||
);
|
||||
console.error(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => setE2eeSetupComplete(true));
|
||||
} else {
|
||||
setE2eeSetupComplete(true);
|
||||
}
|
||||
}, [e2eeEnabled, room, e2eePassphrase]);
|
||||
|
||||
const connectOptions = React.useMemo((): RoomConnectOptions => {
|
||||
return {
|
||||
autoSubscribe: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
room.on(RoomEvent.Disconnected, handleOnLeave);
|
||||
room.on(RoomEvent.EncryptionError, handleEncryptionError);
|
||||
room.on(RoomEvent.MediaDevicesError, handleError);
|
||||
|
||||
if (e2eeSetupComplete) {
|
||||
room
|
||||
.connect(
|
||||
props.connectionDetails.serverUrl,
|
||||
props.connectionDetails.participantToken,
|
||||
connectOptions,
|
||||
)
|
||||
.catch((error) => {
|
||||
handleError(error);
|
||||
});
|
||||
if (props.userChoices.videoEnabled) {
|
||||
room.localParticipant.setCameraEnabled(true).catch((error) => {
|
||||
handleError(error);
|
||||
});
|
||||
}
|
||||
if (props.userChoices.audioEnabled) {
|
||||
room.localParticipant.setMicrophoneEnabled(true).catch((error) => {
|
||||
handleError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
room.off(RoomEvent.Disconnected, handleOnLeave);
|
||||
room.off(RoomEvent.EncryptionError, handleEncryptionError);
|
||||
room.off(RoomEvent.MediaDevicesError, handleError);
|
||||
};
|
||||
}, [e2eeSetupComplete, room, props.connectionDetails, props.userChoices]);
|
||||
|
||||
const lowPowerMode = useLowCPUOptimizer(room);
|
||||
|
||||
const router = useRouter();
|
||||
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
|
||||
const handleError = React.useCallback((error: Error) => {
|
||||
console.error(error);
|
||||
alert(`Encountered an unexpected error, check the console logs for details: ${error.message}`);
|
||||
}, []);
|
||||
const handleEncryptionError = React.useCallback((error: Error) => {
|
||||
console.error(error);
|
||||
alert(
|
||||
`Encountered an unexpected encryption error, check the console logs for details: ${error.message}`,
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (lowPowerMode) {
|
||||
console.warn('Low power mode enabled');
|
||||
}
|
||||
}, [lowPowerMode]);
|
||||
|
||||
return (
|
||||
<div className="lk-room-container">
|
||||
<RoomContext.Provider value={room}>
|
||||
<KeyboardShortcuts />
|
||||
<VideoConference
|
||||
chatMessageFormatter={formatChatMessageLinks}
|
||||
SettingsComponent={SHOW_SETTINGS_MENU ? SettingsMenu : undefined}
|
||||
/>
|
||||
<DebugMode />
|
||||
<RecordingIndicator />
|
||||
</RoomContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
33
app/rooms/[roomName]/page.tsx
Normal file
33
app/rooms/[roomName]/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { PageClientImpl } from './PageClientImpl';
|
||||
import { isVideoCodec } from '@/lib/types';
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ roomName: string }>;
|
||||
searchParams: Promise<{
|
||||
// FIXME: We should not allow values for regions if in playground mode.
|
||||
region?: string;
|
||||
hq?: string;
|
||||
codec?: string;
|
||||
}>;
|
||||
}) {
|
||||
const _params = await params;
|
||||
const _searchParams = await searchParams;
|
||||
const codec =
|
||||
typeof _searchParams.codec === 'string' && isVideoCodec(_searchParams.codec)
|
||||
? _searchParams.codec
|
||||
: 'vp9';
|
||||
const hq = _searchParams.hq === 'true' ? true : false;
|
||||
|
||||
return (
|
||||
<PageClientImpl
|
||||
roomName={_params.roomName}
|
||||
region={_searchParams.region}
|
||||
hq={hq}
|
||||
codec={codec}
|
||||
/>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user