'use client'; import { useState, useEffect, useMemo, useCallback } from 'react'; import Dropdown from '@/components/Dropdown'; import { useToast } from '@/lib/useToast'; import { ToastContainer } from '@/components/Toast'; import { useActiveSourceLookup, useDebounce, PerformanceMonitor } from '@/lib/performance'; import { SCREEN_POSITIONS } from '@/lib/constants'; import { StreamWithTeam } from '@/types'; type ScreenType = typeof SCREEN_POSITIONS[number]; export default function Home() { const [streams, setStreams] = useState([]); const [activeSources, setActiveSources] = useState>( Object.fromEntries(SCREEN_POSITIONS.map(screen => [screen, null])) as Record ); const [isLoading, setIsLoading] = useState(true); const [openDropdown, setOpenDropdown] = useState(null); const [currentScene, setCurrentScene] = useState(null); const [currentPreviewScene, setCurrentPreviewScene] = useState(null); const [studioModeEnabled, setStudioModeEnabled] = useState(false); const { toasts, removeToast, showSuccess, showError } = useToast(); // Memoized active source lookup for performance const activeSourceIds = useActiveSourceLookup(streams, activeSources); // Debounced API calls to prevent excessive requests const setActiveFunction = useCallback(async (screen: ScreenType, id: number | null) => { if (id) { const selectedStream = streams.find(stream => stream.id === id); try { const endTimer = PerformanceMonitor.startTimer('setActive_api'); const response = await fetch('/api/setActive', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ screen, id }), }); endTimer(); if (!response.ok) { throw new Error('Failed to set active stream'); } showSuccess('Source Updated', `Set ${selectedStream?.name || 'stream'} as active for ${screen}`); } catch (error) { console.error('Error setting active stream:', error); showError('Failed to Update Source', 'Could not set active stream. Please try again.'); // Revert local state on error setActiveSources((prev) => ({ ...prev, [screen]: null, })); } } }, [streams, showError, showSuccess]); const debouncedSetActive = useDebounce(setActiveFunction, 300); const fetchData = useCallback(async () => { const endTimer = PerformanceMonitor.startTimer('fetchData'); try { // Fetch streams, active sources, current scene, and OBS status in parallel const [streamsRes, activeRes, sceneRes, obsStatusRes] = await Promise.all([ fetch('/api/streams'), fetch('/api/getActive'), fetch('/api/getCurrentScene'), fetch('/api/obsStatus') ]); const [streamsData, activeData, sceneData, obsStatusData] = await Promise.all([ streamsRes.json(), activeRes.json(), sceneRes.json(), obsStatusRes.json() ]); // Handle both old and new API response formats const streams = streamsData.success ? streamsData.data : streamsData; const activeSources = activeData.success ? activeData.data : activeData; const sceneName = sceneData.success ? sceneData.data.sceneName : null; setStreams(streams); setActiveSources(activeSources); setCurrentScene(sceneName); // Update studio mode and preview scene from OBS status if (obsStatusData.connected) { setStudioModeEnabled(obsStatusData.studioModeEnabled || false); setCurrentPreviewScene(obsStatusData.currentPreviewScene || null); } else { setStudioModeEnabled(false); setCurrentPreviewScene(null); } } catch (error) { console.error('Error fetching data:', error); showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.'); } finally { setIsLoading(false); endTimer(); } }, [showError]); useEffect(() => { fetchData(); }, [fetchData]); const handleSetActive = useCallback(async (screen: ScreenType, id: number | null) => { const selectedStream = streams.find((stream) => stream.id === id); // Generate stream group name for optimistic updates - must match obsClient.js format const streamGroupName = selectedStream ? `${selectedStream.team_name?.toLowerCase().replace(/\s+/g, '_') || 'unknown'}_${selectedStream.name.toLowerCase().replace(/\s+/g, '_')}_stream` : null; // Update local state immediately for optimistic updates setActiveSources((prev) => ({ ...prev, [screen]: streamGroupName, })); // Debounced backend update debouncedSetActive(screen, id); }, [streams, debouncedSetActive]); const handleToggleDropdown = useCallback((screen: string) => { setOpenDropdown((prev) => (prev === screen ? null : screen)); }, []); const handleSceneSwitch = useCallback(async (sceneName: string) => { try { const response = await fetch('/api/setScene', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sceneName }), }); const result = await response.json(); if (result.success) { // Update local state based on studio mode if (result.data.studioMode) { // In studio mode, update preview scene setCurrentPreviewScene(sceneName); showSuccess('Preview Set', result.message); } else { // In normal mode, update program scene setCurrentScene(sceneName); showSuccess('Scene Changed', `Switched to ${sceneName} layout`); } } else { throw new Error(result.error || 'Failed to switch scene'); } } catch (error) { console.error('Error switching scene:', error); showError('Scene Switch Failed', error instanceof Error ? error.message : 'Could not switch scene. Please try again.'); } }, [showSuccess, showError]); const handleTransition = useCallback(async () => { try { const response = await fetch('/api/triggerTransition', { method: 'POST', }); const result = await response.json(); if (result.success) { // Update local state after successful transition setCurrentScene(result.data.programScene); setCurrentPreviewScene(result.data.previewScene); showSuccess('Transition Complete', 'Successfully transitioned preview to program'); // Refresh data to ensure UI is in sync fetchData(); } else { throw new Error(result.error || 'Failed to trigger transition'); } } catch (error) { console.error('Error triggering transition:', error); showError('Transition Failed', error instanceof Error ? error.message : 'Could not trigger transition. Please try again.'); } }, [showSuccess, showError, fetchData]); // Helper function to get scene button state and styling const getSceneButtonState = useCallback((sceneName: string) => { const isProgram = currentScene === sceneName; const isPreview = studioModeEnabled && currentPreviewScene === sceneName; if (studioModeEnabled) { if (isProgram && isPreview) { return { isActive: true, text: `Program & Preview: ${sceneName}`, className: 'active', showTransition: false }; } else if (isProgram) { return { isActive: true, text: `Program: ${sceneName}`, className: 'active', showTransition: false }; } else if (isPreview) { return { isActive: true, text: `Preview: ${sceneName}`, className: 'btn-scene-preview', showTransition: true }; } else { return { isActive: false, text: `Set Preview: ${sceneName}`, className: '', showTransition: false }; } } else { // Normal mode if (isProgram) { return { isActive: true, text: `Active: ${sceneName}`, className: 'active', showTransition: false }; } else { return { isActive: false, text: `Switch to ${sceneName}`, className: '', showTransition: false }; } } }, [currentScene, currentPreviewScene, studioModeEnabled]); // Memoized corner displays to prevent re-renders const cornerDisplays = useMemo(() => [ { screen: 'top_left' as const, label: 'Top Left' }, { screen: 'top_right' as const, label: 'Top Right' }, { screen: 'bottom_left' as const, label: 'Bottom Left' }, { screen: 'bottom_right' as const, label: 'Bottom Right' }, ], []); // Transform and sort streams for dropdown display const dropdownStreams = useMemo(() => { return streams .map(stream => ({ id: stream.id, name: `${stream.team_name} - ${stream.name}`, originalStream: stream })) .sort((a, b) => a.name.localeCompare(b.name)); }, [streams]); if (isLoading) { return (
Loading streams...
); } return (
{/* Title */}

Live Stream Control Center

Manage your OBS sources across multiple screen positions

{/* Main Screen */}

Primary Display

{(() => { const buttonState = getSceneButtonState('1-Screen'); return ( <> {buttonState.showTransition && ( )} ); })()}
handleSetActive('large', id)} label="Select Primary Stream..." isOpen={openDropdown === 'large'} onToggle={() => handleToggleDropdown('large')} />
{/* Side Displays */}

Side Displays

{(() => { const buttonState = getSceneButtonState('2-Screen'); return ( <> {buttonState.showTransition && ( )} ); })()}

Left Display

handleSetActive('left', id)} label="Select Left Stream..." isOpen={openDropdown === 'left'} onToggle={() => handleToggleDropdown('left')} />

Right Display

handleSetActive('right', id)} label="Select Right Stream..." isOpen={openDropdown === 'right'} onToggle={() => handleToggleDropdown('right')} />
{/* Corner Displays */}

Corner Displays

{(() => { const buttonState = getSceneButtonState('4-Screen'); return ( <> {buttonState.showTransition && ( )} ); })()}
{cornerDisplays.map(({ screen, label }) => (

{label}

handleSetActive(screen, id)} label="Select Stream..." isOpen={openDropdown === screen} onToggle={() => handleToggleDropdown(screen)} />
))}
{/* Toast Notifications */}
); }