diff --git a/app/api/record/start/route.ts b/app/api/record/start/route.ts
index 4d75896..07129ac 100644
--- a/app/api/record/start/route.ts
+++ b/app/api/record/start/route.ts
@@ -1,10 +1,11 @@
-import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk';
+import { EgressClient, EncodedFileOutput, RoomServiceClient, S3Upload } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
try {
const roomName = req.nextUrl.searchParams.get('roomName');
const now = req.nextUrl.searchParams.get('now');
+ const identity = req.nextUrl.searchParams.get('identity');
// new Date(Date.now()).toISOString();
/**
@@ -68,6 +69,11 @@ export async function GET(req: NextRequest) {
layout: 'speaker',
},
);
+ const roomClient = new RoomServiceClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
+ await roomClient.updateRoomMetadata(
+ roomName,
+ JSON.stringify({ recording: { isRecording: true, recorder: identity } }),
+ );
if (RUNNER_URL && RUNNER_SECRET) {
post_runner(RUNNER_URL, RUNNER_SECRET, filepath);
@@ -76,6 +82,7 @@ export async function GET(req: NextRequest) {
return new NextResponse(null, { status: 200 });
} catch (error) {
if (error instanceof Error) {
+ console.log({ error });
return new NextResponse(error.message, { status: 500 });
}
}
diff --git a/app/api/record/stop/route.ts b/app/api/record/stop/route.ts
index e2630ac..c9bfd7f 100644
--- a/app/api/record/stop/route.ts
+++ b/app/api/record/stop/route.ts
@@ -1,9 +1,10 @@
-import { EgressClient } from 'livekit-server-sdk';
+import { EgressClient, RoomServiceClient } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
try {
const roomName = req.nextUrl.searchParams.get('roomName');
+ const identity = req.nextUrl.searchParams.get('identity');
/**
* CAUTION:
@@ -22,6 +23,7 @@ export async function GET(req: NextRequest) {
hostURL.protocol = 'https:';
const egressClient = new EgressClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
+ const roomClient = new RoomServiceClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
const activeEgresses = (await egressClient.listEgress({ roomName })).filter(
(info) => info.status < 2,
);
@@ -29,6 +31,10 @@ export async function GET(req: NextRequest) {
return new NextResponse('No active recording found', { status: 404 });
}
await Promise.all(activeEgresses.map((info) => egressClient.stopEgress(info.egressId)));
+ await roomClient.updateRoomMetadata(
+ roomName,
+ JSON.stringify({ recording: { isRecording: false, recorder: identity } }),
+ );
return new NextResponse(null, { status: 200 });
} catch (error) {
diff --git a/app/contexts/layout-context.tsx b/app/contexts/layout-context.tsx
index b031d46..b9ea6da 100644
--- a/app/contexts/layout-context.tsx
+++ b/app/contexts/layout-context.tsx
@@ -2,7 +2,7 @@ import { createContext, useContext } from 'react';
type LayoutContextType = {
// isSettingsOpen: SettingsContextType,
- // isChatOpen: ChatContextType,
+ isChatOpen: ChatContextType;
isParticipantsListOpen: ParticipantsListContextType;
};
diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx
index 87055bf..27d2389 100644
--- a/app/custom/CustomControlBar.tsx
+++ b/app/custom/CustomControlBar.tsx
@@ -1,7 +1,13 @@
'use client';
-import React, { useState, useEffect } from 'react';
-import { DisconnectButton, useLayoutContext, useRoomContext } from '@livekit/components-react';
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ DisconnectButton,
+ useIsRecording,
+ useLayoutContext,
+ useLocalParticipant,
+ useRoomContext,
+} from '@livekit/components-react';
import { Room, RoomEvent, Track } from 'livekit-client';
import { mergeClasses } from '@/lib/client-utils';
import { ToggleSource } from '@livekit/components-core';
@@ -10,6 +16,7 @@ import { CameraOffSVG, CameraOnSVG } from '../svg/camera';
import { MicOffSVG, MicOnSVG } from '../svg/mic';
import { ScreenShareOnSVG } from '../svg/screen-share';
import { useCustomLayoutContext } from '../contexts/layout-context';
+import { useToast } from './toast/use-toast';
interface CustomControlBarProps {
room: Room;
@@ -17,19 +24,101 @@ interface CustomControlBarProps {
}
export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
- const [recording, setRecording] = useState(false);
+ const recordingEndpoint = process.env.NEXT_PUBLIC_LK_RECORD_ENDPOINT;
+ const { localParticipant } = useLocalParticipant();
+ const [isRecordingRequestPending, setIsRecordingRequestPending] = useState(false);
const [participantCount, setParticipantCount] = useState(1);
const { dispatch } = useLayoutContext().widget;
const { isParticipantsListOpen } = useCustomLayoutContext();
+ const { toast } = useToast();
+ const [recordingState, setRecordingState] = useState({
+ recording: { isRecording: false, recorder: '' },
+ });
+ const isRecording = useMemo(() => {
+ return recordingState.recording.isRecording;
+ }, [recordingState]);
+ const isSelfRecord = useMemo(() => {
+ return recordingState.recording.recorder === localParticipant.identity;
+ }, [recordingState]);
+
+ const [isFirstMount, setIsFirstMount] = useState(true);
+
+ useEffect(() => {
+ setIsFirstMount(false);
+ }, []);
+
+ useEffect(() => {
+ if (isRecording) {
+ toast({
+ title: 'Recording in progress. Please be aware this call is being recorded.',
+ });
+ } else {
+ if (isFirstMount) return;
+ toast({
+ title: 'Recorded ended. This call is no longer being recorded.',
+ });
+ }
+ }, [isRecording]);
function ToggleParticipantsList() {
if (isParticipantsListOpen.dispatch)
isParticipantsListOpen.dispatch({ msg: 'toggle_participants_list' });
}
+ const toggleRoomRecording = async () => {
+ if (isRecordingRequestPending || (isRecording && !isSelfRecord)) return;
+ setIsRecordingRequestPending(true);
+ if (!isRecording)
+ toast({
+ title: 'Starting call recording. Please wait...',
+ });
+ else
+ toast({
+ title: 'Stopping call recording. Please wait...',
+ });
+
+ if (!recordingEndpoint) {
+ throw TypeError('No recording endpoint specified');
+ }
+ if (room.isE2EEEnabled) {
+ throw Error('Recording of encrypted meetings is currently not supported');
+ }
+ let response: Response;
+ const now = new Date(Date.now()).toISOString();
+ // const fileName = `${now}-${room.name}.mp4`;
+ if (isRecording) {
+ response = await fetch(
+ recordingEndpoint + `/stop?roomName=${room.name}&identity=${localParticipant.identity}`,
+ );
+ } else {
+ response = await fetch(
+ recordingEndpoint +
+ `/start?roomName=${room.name}&now=${now}&identity=${localParticipant.identity}`,
+ );
+ }
+ if (response.ok) {
+ } else {
+ console.error(
+ 'Error handling recording request, check server logs:',
+ response.status,
+ response.statusText,
+ );
+ }
+ };
+
+ const updateRoomMetadata = (metadata: string) => {
+ const parsedMetadata = JSON.parse(metadata === '' ? '{}' : metadata);
+ setIsRecordingRequestPending(false);
+ setRecordingState({
+ recording: {
+ isRecording: parsedMetadata.recording.isRecording,
+ recorder: parsedMetadata.recording.recorder,
+ },
+ });
+ };
+
useEffect(() => {
if (room) {
- const updateRecordingStatus = () => setRecording(room.isRecording);
const updateParticipantCount = () => {
setParticipantCount(room.numParticipants);
};
@@ -37,13 +126,13 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
room.on(RoomEvent.Connected, updateParticipantCount);
room.on(RoomEvent.ParticipantConnected, updateParticipantCount);
room.on(RoomEvent.ParticipantDisconnected, updateParticipantCount);
- room.on(RoomEvent.RecordingStatusChanged, updateRecordingStatus);
+ room.on(RoomEvent.RoomMetadataChanged, updateRoomMetadata);
return () => {
room.off(RoomEvent.Connected, updateParticipantCount);
room.off(RoomEvent.ParticipantConnected, updateParticipantCount);
room.off(RoomEvent.ParticipantDisconnected, updateParticipantCount);
- room.off(RoomEvent.RecordingStatusChanged, updateRecordingStatus);
+ room.off(RoomEvent.RoomMetadataChanged, updateRoomMetadata);
};
}
}, [room]);
@@ -69,8 +158,18 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {