diff --git a/app/api/record/start/route.ts b/app/api/record/start/route.ts index 1cda0c4..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); 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 82e18b7..25563c7 100644 --- a/app/custom/CustomControlBar.tsx +++ b/app/custom/CustomControlBar.tsx @@ -1,10 +1,11 @@ 'use client'; -import React, { useState, useEffect } from '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'; @@ -24,22 +25,35 @@ interface CustomControlBarProps { export function CustomControlBar({ room, roomName }: CustomControlBarProps) { const recordingEndpoint = process.env.NEXT_PUBLIC_LK_RECORD_ENDPOINT; - const isRecording = useIsRecording(); + 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(() => { - if (isFirstMount) return setIsFirstMount(false); - setIsRecordingRequestPending(false); + 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.', }); @@ -52,7 +66,7 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { } const toggleRoomRecording = async () => { - if (isRecordingRequestPending) return; + if (isRecordingRequestPending || (isRecording && !isSelfRecord)) return; setIsRecordingRequestPending(true); if (!isRecording) toast({ @@ -73,9 +87,14 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { const now = new Date(Date.now()).toISOString(); // const fileName = `${now}-${room.name}.mp4`; if (isRecording) { - response = await fetch(recordingEndpoint + `/stop?roomName=${room.name}`); + response = await fetch( + recordingEndpoint + `/stop?roomName=${room.name}&identity=${localParticipant.identity}`, + ); } else { - response = await fetch(recordingEndpoint + `/start?roomName=${room.name}&now=${now}`); + response = await fetch( + recordingEndpoint + + `/start?roomName=${room.name}&now=${now}&identity=${localParticipant.identity}`, + ); } if (response.ok) { } else { @@ -87,6 +106,17 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { } }; + 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 updateParticipantCount = () => { @@ -96,11 +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.RoomMetadataChanged, updateRoomMetadata); return () => { room.off(RoomEvent.Connected, updateParticipantCount); room.off(RoomEvent.ParticipantConnected, updateParticipantCount); room.off(RoomEvent.ParticipantDisconnected, updateParticipantCount); + room.off(RoomEvent.RoomMetadataChanged, updateRoomMetadata); }; } }, [room]); @@ -127,18 +159,16 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
{isRecording ? ( - - stop_circle - + stop_circle ) : ( radio_button_checked )} diff --git a/lib/RecordingIndicator.tsx b/lib/RecordingIndicator.tsx index 5463303..6d074c9 100644 --- a/lib/RecordingIndicator.tsx +++ b/lib/RecordingIndicator.tsx @@ -1,8 +1,35 @@ -import { useIsRecording } from '@livekit/components-react'; +import { useRoomContext } from '@livekit/components-react'; +import { RoomEvent } from 'livekit-client'; import * as React from 'react'; export function RecordingIndicator() { - const isRecording = useIsRecording(); + const [recordingState, setRecordingState] = React.useState({ + recording: { isRecording: false, recorder: '' }, + }); + const isRecording = React.useMemo(() => { + return recordingState.recording.isRecording; + }, [recordingState]); + const room = useRoomContext(); + + const updateRoomMetadata = (metadata: string) => { + const parsedMetadata = JSON.parse(metadata === '' ? '{}' : metadata); + setRecordingState({ + recording: { + isRecording: parsedMetadata.recording.isRecording, + recorder: parsedMetadata.recording.recorder, + }, + }); + }; + + React.useEffect(() => { + if (room) { + room.on(RoomEvent.RoomMetadataChanged, updateRoomMetadata); + + return () => { + room.off(RoomEvent.RoomMetadataChanged, updateRoomMetadata); + }; + } + }, [room]); return (