...
This commit is contained in:
		
							
								
								
									
										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