diff --git a/.env.example b/.env.example index c0b8aa0..937b7d3 100644 --- a/.env.example +++ b/.env.example @@ -8,10 +8,21 @@ LIVEKIT_API_SECRET=secret # URL pointing to the LiveKit server. LIVEKIT_URL=wss://my-livekit-project.livekit.cloud -## PUBLIC +# Recording +# S3_KEY_ID= +# S3_KEY_SECRET= +# S3_ENDPOINT= +# S3_BUCKET= +# S3_REGION= + +# 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. # NEXT_PUBLIC_SHOW_SETTINGS_MENU=true + # Optional, to pipe logs to datadog # NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=client-token # NEXT_PUBLIC_DATADOG_SITE=datadog-site + diff --git a/lib/RecordingIndicator.tsx b/lib/RecordingIndicator.tsx new file mode 100644 index 0000000..877ed9a --- /dev/null +++ b/lib/RecordingIndicator.tsx @@ -0,0 +1,30 @@ +import { useIsRecording } from '@livekit/components-react'; +import * as React from 'react'; + +export function RecordingIndicator() { + const isRecording = useIsRecording(); + const [wasRecording, setWasRecording] = React.useState(false); + + React.useEffect(() => { + if (isRecording !== wasRecording) { + setWasRecording(isRecording); + if (isRecording) { + window.alert('This meeting is being recorded'); + } + } + }, [isRecording]); + + return ( +
+ ); +} diff --git a/lib/SettingsMenu.tsx b/lib/SettingsMenu.tsx index 41655dd..9951d34 100644 --- a/lib/SettingsMenu.tsx +++ b/lib/SettingsMenu.tsx @@ -7,6 +7,7 @@ import { MediaDeviceMenu, TrackToggle, useRoomContext, + useIsRecording, } from '@livekit/components-react'; import styles from '../styles/SettingsMenu.module.css'; @@ -21,16 +22,18 @@ export interface SettingsMenuProps extends React.HTMLAttributes export function SettingsMenu(props: SettingsMenuProps) { const layoutContext = useMaybeLayoutContext(); const room = useRoomContext(); + const recordingEndpoint = process.env.NEXT_PUBLIC_LK_RECORD_ENDPOINT; const settings = React.useMemo(() => { return { media: { camera: true, microphone: true, label: 'Media Devices', speaker: true }, effects: { label: 'Effects' }, + recording: recordingEndpoint ? { label: 'Recording' } : undefined, }; }, []); const tabs = React.useMemo( - () => Object.keys(settings) as Array, + () => Object.keys(settings).filter((t) => t !== undefined) as Array, [settings], ); const { microphoneTrack } = useLocalParticipant(); @@ -62,6 +65,42 @@ export function SettingsMenu(props: SettingsMenuProps) { } }, [isNoiseFilterEnabled, microphoneTrack]); + const isRecording = useIsRecording(); + const [initialRecStatus, setInitialRecStatus] = React.useState(isRecording); + const [processingRecRequest, setProcessingRecRequest] = React.useState(false); + + React.useEffect(() => { + if (initialRecStatus !== isRecording) { + setProcessingRecRequest(false); + } + }, [isRecording, initialRecStatus]); + + const toggleRoomRecording = async () => { + if (!recordingEndpoint) { + throw TypeError('No recording endpoint specified'); + } + if (room.isE2EEEnabled) { + throw Error('Recording of encrypted meetings is currently not supported'); + } + setProcessingRecRequest(true); + setInitialRecStatus(isRecording); + let response: Response; + if (isRecording) { + response = await fetch(recordingEndpoint + `/stop?roomName=${room.name}`); + } else { + response = await fetch(recordingEndpoint + `/start?roomName=${room.name}`); + } + if (response.ok) { + } else { + console.error( + 'Error handling recording request, check server logs:', + response.status, + response.statusText, + ); + setProcessingRecRequest(false); + } + }; + return (
@@ -134,6 +173,21 @@ export function SettingsMenu(props: SettingsMenuProps) { )} + {activeTab === 'recording' && ( + <> +

Record Meeting

+
+

+ {isRecording + ? 'Meeting is currently being recorded' + : 'No active recordings for this meeting'} +

+ +
+ + )}