Added global state for isRecording and recorder for record button function
This commit is contained in:
parent
1947856c08
commit
9734b5e1e3
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -2,7 +2,7 @@ import { createContext, useContext } from 'react';
|
||||
|
||||
type LayoutContextType = {
|
||||
// isSettingsOpen: SettingsContextType,
|
||||
// isChatOpen: ChatContextType,
|
||||
isChatOpen: ChatContextType;
|
||||
isParticipantsListOpen: ParticipantsListContextType;
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user