You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
6.3 KiB
204 lines
6.3 KiB
1 month ago
|
'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 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 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 [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;
|
||
|
}
|
||
|
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,
|
||
|
};
|
||
|
}, [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,
|
||
|
};
|
||
|
}, []);
|
||
|
|
||
|
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}`,
|
||
|
);
|
||
|
}, []);
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
<LiveKitRoom
|
||
|
connect={e2eeSetupComplete}
|
||
|
room={room}
|
||
|
token={props.connectionDetails.participantToken}
|
||
|
serverUrl={props.connectionDetails.serverUrl}
|
||
|
connectOptions={connectOptions}
|
||
|
video={props.userChoices.videoEnabled}
|
||
|
audio={props.userChoices.audioEnabled}
|
||
|
onDisconnected={handleOnLeave}
|
||
|
onEncryptionError={handleEncryptionError}
|
||
|
onError={handleError}
|
||
|
>
|
||
|
<VideoConference
|
||
|
chatMessageFormatter={formatChatMessageLinks}
|
||
|
SettingsComponent={SHOW_SETTINGS_MENU ? SettingsMenu : undefined}
|
||
|
/>
|
||
|
<DebugMode />
|
||
|
<RecordingIndicator />
|
||
|
</LiveKitRoom>
|
||
|
</>
|
||
|
);
|
||
|
}
|