'use client'; import React, { useState, useEffect, useMemo } from 'react'; import { DisconnectButton, 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'; import '../../styles/CustomControlBar.css'; 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'; export function CustomControlBar() { const room = useRoomContext(); const recordingEndpoint = process.env.NEXT_PUBLIC_LK_RECORD_ENDPOINT; const { localParticipant } = useLocalParticipant(); const [isRecordingRequestPending, setIsRecordingRequestPending] = useState(false); const [participantCount, setParticipantCount] = useState(room.numParticipants); const { dispatch } = useLayoutContext().widget; const { isParticipantsListOpen, isChatOpen } = 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 toggleChat = () => { if (isChatOpen.dispatch) isChatOpen.dispatch({ msg: 'toggle_chat' }); }; 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 updateParticipantCount = () => { setParticipantCount(room.numParticipants); }; 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]); const handleCopyLink = () => { navigator.clipboard .writeText(window.location.href) .then(() => alert('Link copied to clipboard!')) .catch((err) => console.error('Failed to copy link:', err)); }; return (
{room.name}
{/* Center: Control Buttons */}
{isRecording ? ( stop_circle ) : ( radio_button_checked )}
call_end
{/* Participants, Settings btn */}
people {participantCount}
chat
{ if (dispatch) dispatch({ msg: 'toggle_settings' }); }} > settings
); } interface ControlButtonProps { enabled?: boolean; icon: React.ReactNode; className?: string; onClick?: () => void; } function ControlButton({ enabled = true, icon, className, onClick }: ControlButtonProps) { return ( ); } function TrackToggle({ source }: { source: ToggleSource }) { const { enabled, toggle } = useTrackToggle({ source }); const isScreenShare = source === Track.Source.ScreenShare; return ( } /> ); } interface TrackIconProps { trackSource: ToggleSource; enabled: boolean; } function TrackIcon({ trackSource, enabled }: TrackIconProps) { switch (trackSource) { case Track.Source.Camera: return enabled ? : ; case Track.Source.Microphone: return enabled ? : ; case Track.Source.ScreenShare: return enabled ? ( ) : ( stop_screen_share ); } } // Custom hook for track toggle function useTrackToggle({ source }: { source: ToggleSource }) { const { localParticipant } = useRoomContext(); const toggle = () => { switch (source) { case Track.Source.Camera: return localParticipant.setCameraEnabled(!enabled); case Track.Source.Microphone: return localParticipant.setMicrophoneEnabled(!enabled); case Track.Source.ScreenShare: return localParticipant.setScreenShareEnabled(!enabled); } }; const enabled = (() => { switch (source) { case Track.Source.Camera: return localParticipant.isCameraEnabled; case Track.Source.Microphone: return localParticipant.isMicrophoneEnabled; case Track.Source.ScreenShare: return localParticipant.isScreenShareEnabled; } })(); return { enabled, toggle }; }