Added global state for isRecording and recorder for record button function

This commit is contained in:
SujithThirumalaisamy 2025-04-06 20:49:17 +05:30
parent 1947856c08
commit 9734b5e1e3
6 changed files with 101 additions and 17 deletions

View File

@ -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);

View File

@ -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) {

View File

@ -2,7 +2,7 @@ import { createContext, useContext } from 'react';
type LayoutContextType = {
// isSettingsOpen: SettingsContextType,
// isChatOpen: ChatContextType,
isChatOpen: ChatContextType;
isParticipantsListOpen: ParticipantsListContextType;
};

View File

@ -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) {
<TrackToggle source={Track.Source.Camera} />
<div
className={`control-btn ${isRecording ? '' : 'disabled'}`}
className={`control-btn ${isRecording ? '' : 'disabled'} ${isRecordingRequestPending || !isSelfRecord ? 'blinking' : ''}`}
onClick={toggleRoomRecording}
data-lk-active={isRecording}
style={{
cursor: isRecordingRequestPending ? 'not-allowed' : 'pointer',
color: isRecordingRequestPending ? 'gray' : isRecording ? '#ED7473' : 'white',
color: isRecording || isRecordingRequestPending ? '#ED7473' : 'white',
}}
>
{isRecording ? (
<span className="material-symbols-outlined" style={{}}>
stop_circle
</span>
<span className="material-symbols-outlined">stop_circle</span>
) : (
<span className="material-symbols-outlined">radio_button_checked</span>
)}

View File

@ -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 (
<div

View File

@ -151,3 +151,18 @@ h2 a {
justify-content: space-between;
gap: 0.5rem;
}
@keyframes blink {
0%,
100% {
background-color: #382327;
}
50% {
background-color: #2d1e22;
}
}
.blinking {
animation: blink 1s infinite;
}