'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( undefined, ); const preJoinDefaults = React.useMemo(() => { return { username: '', videoEnabled: true, audioEnabled: true, }; }, []); const [connectionDetails, setConnectionDetails] = React.useState( 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 (
{connectionDetails === undefined || preJoinChoices === undefined ? (
) : ( )}
); } 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 (
); }