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'}
+
+
+
+ >
+ )}