Refactor app use connection details endpoint (#302)
* update to connection-details endpoint * chore: Update .env.example * chore: Update .env.example * remove fallback logic * chore: Update connection details property names
This commit is contained in:
parent
15e58cd797
commit
f2f4ada03d
18
.env.example
18
.env.example
@ -1,13 +1,17 @@
|
|||||||
# 1. Copy this file and rename it to .env.local
|
# 1. Copy this file and rename it to .env.local
|
||||||
# 2. Update the enviroment variables below.
|
# 2. Update the enviroment variables below.
|
||||||
|
|
||||||
# API key and secret. If you use LiveKit Cloud this can be generated via the cloud dashboard.
|
# REQUIRED SETTINGS
|
||||||
LIVEKIT_API_KEY=devkey
|
# #################
|
||||||
LIVEKIT_API_SECRET=secret
|
# If you are using LiveKit Cloud, the API key and secret can be generated from the Cloud Dashboard.
|
||||||
|
LIVEKIT_API_KEY=
|
||||||
|
LIVEKIT_API_SECRET=
|
||||||
|
# URL pointing to the LiveKit server. (example: `wss://my-livekit-project.livekit.cloud`)
|
||||||
|
LIVEKIT_URL=
|
||||||
|
|
||||||
# URL pointing to the LiveKit server.
|
|
||||||
LIVEKIT_URL=wss://my-livekit-project.livekit.cloud
|
|
||||||
|
|
||||||
|
# OPTIONAL SETTINGS
|
||||||
|
# #################
|
||||||
# Recording
|
# Recording
|
||||||
# S3_KEY_ID=
|
# S3_KEY_ID=
|
||||||
# S3_KEY_SECRET=
|
# S3_KEY_SECRET=
|
||||||
@ -16,11 +20,9 @@ LIVEKIT_URL=wss://my-livekit-project.livekit.cloud
|
|||||||
# S3_REGION=
|
# S3_REGION=
|
||||||
|
|
||||||
# PUBLIC
|
# PUBLIC
|
||||||
NEXT_PUBLIC_LK_TOKEN_ENDPOINT=/api/token
|
|
||||||
#NEXT_PUBLIC_LK_RECORD_ENDPOINT=/api/record
|
|
||||||
|
|
||||||
# Uncomment settings menu when using a LiveKit Cloud, it'll enable Krisp noise filters.
|
# Uncomment settings menu when using a LiveKit Cloud, it'll enable Krisp noise filters.
|
||||||
# NEXT_PUBLIC_SHOW_SETTINGS_MENU=true
|
# NEXT_PUBLIC_SHOW_SETTINGS_MENU=true
|
||||||
|
# NEXT_PUBLIC_LK_RECORD_ENDPOINT=/api/record
|
||||||
|
|
||||||
# Optional, to pipe logs to datadog
|
# Optional, to pipe logs to datadog
|
||||||
# NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=client-token
|
# NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=client-token
|
||||||
|
|||||||
61
app/api/connection-details/route.ts
Normal file
61
app/api/connection-details/route.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ConnectionDetails } from '@/lib/types';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
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;
|
||||||
|
|
||||||
|
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') ?? '';
|
||||||
|
|
||||||
|
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
|
||||||
|
const participantToken = await createParticipantToken(
|
||||||
|
{
|
||||||
|
identity: `${participantName}__${randomUUID()}`,
|
||||||
|
name: participantName,
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
roomName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return connection details
|
||||||
|
const data: ConnectionDetails = {
|
||||||
|
serverUrl: LIVEKIT_URL!,
|
||||||
|
roomName: roomName,
|
||||||
|
participantToken: participantToken,
|
||||||
|
participantName: participantName,
|
||||||
|
};
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
@ -1,11 +1,9 @@
|
|||||||
import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk';
|
import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url);
|
const roomName = req.nextUrl.searchParams.get('roomName');
|
||||||
const searchParams = url.searchParams;
|
|
||||||
const roomName = searchParams.get('roomName');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CAUTION:
|
* CAUTION:
|
||||||
@ -14,7 +12,7 @@ export async function GET(req: Request) {
|
|||||||
* DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS
|
* DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (typeof roomName !== 'string') {
|
if (roomName === null) {
|
||||||
return new NextResponse('Missing roomName parameter', { status: 403 });
|
return new NextResponse('Missing roomName parameter', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { EgressClient } from 'livekit-server-sdk';
|
import { EgressClient } from 'livekit-server-sdk';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url);
|
const roomName = req.nextUrl.searchParams.get('roomName');
|
||||||
const searchParams = url.searchParams;
|
|
||||||
const roomName = searchParams.get('roomName');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CAUTION:
|
* CAUTION:
|
||||||
@ -14,7 +12,7 @@ export async function GET(req: Request) {
|
|||||||
* DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS
|
* DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (typeof roomName !== 'string') {
|
if (roomName === null) {
|
||||||
return new NextResponse('Missing roomName parameter', { status: 403 });
|
return new NextResponse('Missing roomName parameter', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
import { AccessToken } from 'livekit-server-sdk';
|
|
||||||
import type { AccessTokenOptions, VideoGrant } from 'livekit-server-sdk';
|
|
||||||
import { TokenResult } from '@/lib/types';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
||||||
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
||||||
|
|
||||||
const createToken = (userInfo: AccessTokenOptions, grant: VideoGrant) => {
|
|
||||||
const at = new AccessToken(apiKey, apiSecret, userInfo);
|
|
||||||
at.ttl = '5m';
|
|
||||||
at.addGrant(grant);
|
|
||||||
return at.toJwt();
|
|
||||||
};
|
|
||||||
|
|
||||||
const roomPattern = /\w{4}\-\w{4}/;
|
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
|
||||||
try {
|
|
||||||
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') {
|
|
||||||
return new NextResponse('Forbidden', { status: 401 });
|
|
||||||
}
|
|
||||||
if (name === null) {
|
|
||||||
return new NextResponse('Provide a name.', { status: 400 });
|
|
||||||
}
|
|
||||||
if (Array.isArray(name)) {
|
|
||||||
return new NextResponse('Provide only one room name.', { status: 400 });
|
|
||||||
}
|
|
||||||
if (Array.isArray(metadata)) {
|
|
||||||
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)) {
|
|
||||||
return new NextResponse('Invalid room name format.', { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const grant: VideoGrant = {
|
|
||||||
room: roomName,
|
|
||||||
roomJoin: true,
|
|
||||||
canPublish: true,
|
|
||||||
canPublishData: true,
|
|
||||||
canSubscribe: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const token = await createToken({ identity, name, metadata }, grant);
|
|
||||||
const result: TokenResult = {
|
|
||||||
identity,
|
|
||||||
accessToken: token,
|
|
||||||
};
|
|
||||||
|
|
||||||
return NextResponse.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return new NextResponse(error.message, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +1,9 @@
|
|||||||
import { getLiveKitURL } from '../../../lib/server-utils';
|
import { getLiveKitURL } from '@/lib/server-utils';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.url);
|
const region = req.nextUrl.searchParams.get('region');
|
||||||
const searchParams = url.searchParams;
|
|
||||||
const region = searchParams.get('region');
|
|
||||||
|
|
||||||
if (Array.isArray(region)) {
|
|
||||||
throw Error('provide max one region string');
|
|
||||||
}
|
|
||||||
const livekitUrl = getLiveKitURL(region);
|
const livekitUrl = getLiveKitURL(region);
|
||||||
return NextResponse.json({ url: livekitUrl });
|
return NextResponse.json({ url: livekitUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
176
app/rooms/[roomName]/PageClientImpl.tsx
Normal file
176
app/rooms/[roomName]/PageClientImpl.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { decodePassphrase } from '@/lib/client-utils';
|
||||||
|
import { DebugMode } from '@/lib/Debug';
|
||||||
|
import { RecordingIndicator } from '@/lib/RecordingIndicator';
|
||||||
|
import { SettingsMenu } from '@/lib/SettingsMenu';
|
||||||
|
import { ConnectionDetails } from '@/lib/types';
|
||||||
|
import {
|
||||||
|
formatChatMessageLinks,
|
||||||
|
LiveKitRoom,
|
||||||
|
LocalUserChoices,
|
||||||
|
PreJoin,
|
||||||
|
VideoConference,
|
||||||
|
} from '@livekit/components-react';
|
||||||
|
import {
|
||||||
|
ExternalE2EEKeyProvider,
|
||||||
|
RoomOptions,
|
||||||
|
VideoCodec,
|
||||||
|
VideoPresets,
|
||||||
|
Room,
|
||||||
|
DeviceUnsupportedError,
|
||||||
|
RoomConnectOptions,
|
||||||
|
} from 'livekit-client';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
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 connectionDetailsResp = await fetch(
|
||||||
|
`${CONN_DETAILS_ENDPOINT}?roomName=${props.roomName}&participantName=${values.username}`,
|
||||||
|
);
|
||||||
|
const connectionDetailsData = await connectionDetailsResp.json();
|
||||||
|
setConnectionDetails(connectionDetailsData);
|
||||||
|
}, []);
|
||||||
|
const handlePreJoinError = React.useCallback((e: any) => console.error(e), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main data-lk-theme="default">
|
||||||
|
{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 e2eePassphrase =
|
||||||
|
typeof window !== 'undefined' && decodePassphrase(location.hash.substring(1));
|
||||||
|
|
||||||
|
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 = props.options.codec ? props.options.codec : 'vp9';
|
||||||
|
if (e2eeEnabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
|
||||||
|
videoCodec = undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
videoCaptureDefaults: {
|
||||||
|
deviceId: props.userChoices.videoDeviceId ?? undefined,
|
||||||
|
resolution: props.options.hq ? VideoPresets.h2160 : VideoPresets.h720,
|
||||||
|
},
|
||||||
|
publishDefaults: {
|
||||||
|
dtx: false,
|
||||||
|
videoSimulcastLayers: props.options.hq
|
||||||
|
? [VideoPresets.h1080, VideoPresets.h720]
|
||||||
|
: [VideoPresets.h540, VideoPresets.h216],
|
||||||
|
red: !e2eeEnabled,
|
||||||
|
videoCodec,
|
||||||
|
},
|
||||||
|
audioCaptureDefaults: {
|
||||||
|
deviceId: props.userChoices.audioDeviceId ?? undefined,
|
||||||
|
},
|
||||||
|
adaptiveStream: { pixelDensity: 'screen' },
|
||||||
|
dynacast: true,
|
||||||
|
e2ee: e2eeEnabled
|
||||||
|
? {
|
||||||
|
keyProvider,
|
||||||
|
worker,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
setLogLevel('debug', 'lk-e2ee');
|
||||||
|
}, [props.userChoices, props.options.hq, props.options.codec]);
|
||||||
|
|
||||||
|
const room = React.useMemo(() => new Room(roomOptions), []);
|
||||||
|
|
||||||
|
if (e2eeEnabled) {
|
||||||
|
keyProvider.setKey(decodePassphrase(e2eePassphrase));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const connectOptions = React.useMemo((): RoomConnectOptions => {
|
||||||
|
return {
|
||||||
|
autoSubscribe: true,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LiveKitRoom
|
||||||
|
room={room}
|
||||||
|
token={props.connectionDetails.participantToken}
|
||||||
|
serverUrl={props.connectionDetails.serverUrl}
|
||||||
|
connectOptions={connectOptions}
|
||||||
|
video={props.userChoices.videoEnabled}
|
||||||
|
audio={props.userChoices.audioEnabled}
|
||||||
|
onDisconnected={handleOnLeave}
|
||||||
|
>
|
||||||
|
<VideoConference
|
||||||
|
chatMessageFormatter={formatChatMessageLinks}
|
||||||
|
SettingsComponent={SHOW_SETTINGS_MENU ? SettingsMenu : undefined}
|
||||||
|
/>
|
||||||
|
<DebugMode />
|
||||||
|
<RecordingIndicator />
|
||||||
|
</LiveKitRoom>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,180 +1,26 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
LiveKitRoom,
|
|
||||||
VideoConference,
|
|
||||||
formatChatMessageLinks,
|
|
||||||
useToken,
|
|
||||||
LocalUserChoices,
|
|
||||||
PreJoin,
|
|
||||||
} from '@livekit/components-react';
|
|
||||||
import {
|
|
||||||
DeviceUnsupportedError,
|
|
||||||
ExternalE2EEKeyProvider,
|
|
||||||
Room,
|
|
||||||
RoomConnectOptions,
|
|
||||||
RoomOptions,
|
|
||||||
VideoCodec,
|
|
||||||
VideoPresets,
|
|
||||||
setLogLevel,
|
|
||||||
} from 'livekit-client';
|
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { DebugMode } from '@/lib/Debug';
|
import { PageClientImpl } from './PageClientImpl';
|
||||||
import { decodePassphrase, useServerUrl } from '@/lib/client-utils';
|
|
||||||
import { SettingsMenu } from '@/lib/SettingsMenu';
|
|
||||||
import { RecordingIndicator } from '@/lib/RecordingIndicator';
|
|
||||||
import { isVideoCodec } from '@/lib/types';
|
import { isVideoCodec } from '@/lib/types';
|
||||||
|
|
||||||
export default function Page({ params }: { params: { roomName: string } }) {
|
export default function Page({
|
||||||
const router = useRouter();
|
params,
|
||||||
const roomName = params.roomName;
|
searchParams,
|
||||||
const [preJoinChoices, setPreJoinChoices] = React.useState<LocalUserChoices | undefined>(
|
}: {
|
||||||
undefined,
|
params: { roomName: string };
|
||||||
);
|
searchParams: {
|
||||||
const preJoinDefaults = React.useMemo(() => {
|
// FIXME: We should not allow values for regions if in playground mode.
|
||||||
return {
|
region?: string;
|
||||||
username: '',
|
hq?: string;
|
||||||
videoEnabled: true,
|
codec?: string;
|
||||||
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 (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ActiveRoom(props: {
|
|
||||||
userChoices: LocalUserChoices;
|
|
||||||
roomName: string;
|
|
||||||
onLeave: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
const searchParams = useSearchParams();
|
const codec =
|
||||||
const region = searchParams?.get('region');
|
typeof searchParams.codec === 'string' && isVideoCodec(searchParams.codec)
|
||||||
const hq = searchParams?.get('hq');
|
? searchParams.codec
|
||||||
const codec = searchParams?.get('codec');
|
: 'vp9';
|
||||||
|
const hq = searchParams.hq === 'true' ? true : false;
|
||||||
const tokenOptions = React.useMemo(() => {
|
|
||||||
return {
|
|
||||||
userInfo: {
|
|
||||||
identity: props.userChoices.username,
|
|
||||||
name: props.userChoices.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [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(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 = codec && isVideoCodec(codec) ? codec : 'vp9';
|
|
||||||
if (e2eeEnabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
|
|
||||||
videoCodec = undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
videoCaptureDefaults: {
|
|
||||||
deviceId: props.userChoices.videoDeviceId ?? undefined,
|
|
||||||
resolution: hq === 'true' ? VideoPresets.h2160 : VideoPresets.h720,
|
|
||||||
},
|
|
||||||
publishDefaults: {
|
|
||||||
dtx: false,
|
|
||||||
videoSimulcastLayers:
|
|
||||||
hq === 'true'
|
|
||||||
? [VideoPresets.h1080, VideoPresets.h720]
|
|
||||||
: [VideoPresets.h540, VideoPresets.h216],
|
|
||||||
red: !e2eeEnabled,
|
|
||||||
videoCodec,
|
|
||||||
},
|
|
||||||
audioCaptureDefaults: {
|
|
||||||
deviceId: props.userChoices.audioDeviceId ?? undefined,
|
|
||||||
},
|
|
||||||
adaptiveStream: { pixelDensity: 'screen' },
|
|
||||||
dynacast: true,
|
|
||||||
e2ee: e2eeEnabled
|
|
||||||
? {
|
|
||||||
keyProvider,
|
|
||||||
worker,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
// @ts-ignore
|
|
||||||
setLogLevel('debug', 'lk-e2ee');
|
|
||||||
}, [props.userChoices, hq, codec]);
|
|
||||||
|
|
||||||
const room = React.useMemo(() => new Room(roomOptions), []);
|
|
||||||
|
|
||||||
if (e2eeEnabled) {
|
|
||||||
keyProvider.setKey(decodePassphrase(e2eePassphrase));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const connectOptions = React.useMemo((): RoomConnectOptions => {
|
|
||||||
return {
|
|
||||||
autoSubscribe: true,
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!liveKitUrl) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageClientImpl roomName={params.roomName} region={searchParams.region} hq={hq} codec={codec} />
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,3 +19,10 @@ export interface TokenResult {
|
|||||||
export function isVideoCodec(codec: string): codec is VideoCodec {
|
export function isVideoCodec(codec: string): codec is VideoCodec {
|
||||||
return videoCodecs.includes(codec as VideoCodec);
|
return videoCodecs.includes(codec as VideoCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConnectionDetails = {
|
||||||
|
serverUrl: string;
|
||||||
|
roomName: string;
|
||||||
|
participantName: string;
|
||||||
|
participantToken: string;
|
||||||
|
};
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -2281,7 +2281,7 @@ snapshots:
|
|||||||
eslint: 9.8.0
|
eslint: 9.8.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0)
|
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0)
|
||||||
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.8.0)
|
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0))(eslint@9.8.0)
|
||||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@9.8.0)
|
eslint-plugin-jsx-a11y: 6.8.0(eslint@9.8.0)
|
||||||
eslint-plugin-react: 7.33.2(eslint@9.8.0)
|
eslint-plugin-react: 7.33.2(eslint@9.8.0)
|
||||||
eslint-plugin-react-hooks: 4.6.0(eslint@9.8.0)
|
eslint-plugin-react-hooks: 4.6.0(eslint@9.8.0)
|
||||||
@ -2305,7 +2305,7 @@ snapshots:
|
|||||||
enhanced-resolve: 5.17.1
|
enhanced-resolve: 5.17.1
|
||||||
eslint: 9.8.0
|
eslint: 9.8.0
|
||||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0))(eslint@9.8.0)
|
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0))(eslint@9.8.0)
|
||||||
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.8.0)
|
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0))(eslint@9.8.0)
|
||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
get-tsconfig: 4.7.2
|
get-tsconfig: 4.7.2
|
||||||
is-core-module: 2.13.1
|
is-core-module: 2.13.1
|
||||||
@ -2327,7 +2327,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@9.8.0):
|
eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0(eslint@9.8.0))(eslint@9.8.0))(eslint@9.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.7
|
array-includes: 3.1.7
|
||||||
array.prototype.findlastindex: 1.2.3
|
array.prototype.findlastindex: 1.2.3
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user