Add studio mode status display and preview/program indicators

- Enhanced obsStatus API to include studio mode and preview scene information
- Updated Footer component to show studio mode status (STUDIO/DIRECT)
- Added preview scene display in footer when studio mode is enabled
- Implemented dynamic scene button states showing Program/Preview/Both status
- Scene buttons now clearly indicate preview vs program with distinct colors
- Added proper state management for studio mode and preview scenes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Decobus 2025-07-25 20:52:42 -04:00
parent e777b3d422
commit 07028b0792
3 changed files with 147 additions and 43 deletions

View file

@ -19,9 +19,11 @@ export async function GET() {
obsWebSocketVersion: string; obsWebSocketVersion: string;
}; };
currentScene?: string; currentScene?: string;
currentPreviewScene?: string;
sceneCount?: number; sceneCount?: number;
streaming?: boolean; streaming?: boolean;
recording?: boolean; recording?: boolean;
studioModeEnabled?: boolean;
error?: string; error?: string;
} = { } = {
host: OBS_HOST, host: OBS_HOST,
@ -58,15 +60,31 @@ export async function GET() {
// Get recording status // Get recording status
const recordStatus = await obs.call('GetRecordStatus'); const recordStatus = await obs.call('GetRecordStatus');
// Get studio mode status
const studioModeStatus = await obs.call('GetStudioModeEnabled');
// Get preview scene if studio mode is enabled
let currentPreviewScene;
if (studioModeStatus.studioModeEnabled) {
try {
const previewSceneInfo = await obs.call('GetCurrentPreviewScene');
currentPreviewScene = previewSceneInfo.sceneName;
} catch (previewError) {
console.log('Could not get preview scene:', previewError);
}
}
connectionStatus.connected = true; connectionStatus.connected = true;
connectionStatus.version = { connectionStatus.version = {
obsVersion: versionInfo.obsVersion, obsVersion: versionInfo.obsVersion,
obsWebSocketVersion: versionInfo.obsWebSocketVersion obsWebSocketVersion: versionInfo.obsWebSocketVersion
}; };
connectionStatus.currentScene = currentSceneInfo.sceneName; connectionStatus.currentScene = currentSceneInfo.sceneName;
connectionStatus.currentPreviewScene = currentPreviewScene;
connectionStatus.sceneCount = sceneList.scenes.length; connectionStatus.sceneCount = sceneList.scenes.length;
connectionStatus.streaming = streamStatus.outputActive; connectionStatus.streaming = streamStatus.outputActive;
connectionStatus.recording = recordStatus.outputActive; connectionStatus.recording = recordStatus.outputActive;
connectionStatus.studioModeEnabled = studioModeStatus.studioModeEnabled;
} catch (err) { } catch (err) {
connectionStatus.error = err instanceof Error ? err.message : 'Unknown error occurred'; connectionStatus.error = err instanceof Error ? err.message : 'Unknown error occurred';

View file

@ -19,6 +19,8 @@ export default function Home() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [openDropdown, setOpenDropdown] = useState<string | null>(null); const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const [currentScene, setCurrentScene] = useState<string | null>(null); const [currentScene, setCurrentScene] = useState<string | null>(null);
const [currentPreviewScene, setCurrentPreviewScene] = useState<string | null>(null);
const [studioModeEnabled, setStudioModeEnabled] = useState<boolean>(false);
const { toasts, removeToast, showSuccess, showError } = useToast(); const { toasts, removeToast, showSuccess, showError } = useToast();
// Memoized active source lookup for performance // Memoized active source lookup for performance
@ -59,17 +61,19 @@ export default function Home() {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
const endTimer = PerformanceMonitor.startTimer('fetchData'); const endTimer = PerformanceMonitor.startTimer('fetchData');
try { try {
// Fetch streams, active sources, and current scene in parallel // Fetch streams, active sources, current scene, and OBS status in parallel
const [streamsRes, activeRes, sceneRes] = await Promise.all([ const [streamsRes, activeRes, sceneRes, obsStatusRes] = await Promise.all([
fetch('/api/streams'), fetch('/api/streams'),
fetch('/api/getActive'), fetch('/api/getActive'),
fetch('/api/getCurrentScene') fetch('/api/getCurrentScene'),
fetch('/api/obsStatus')
]); ]);
const [streamsData, activeData, sceneData] = await Promise.all([ const [streamsData, activeData, sceneData, obsStatusData] = await Promise.all([
streamsRes.json(), streamsRes.json(),
activeRes.json(), activeRes.json(),
sceneRes.json() sceneRes.json(),
obsStatusRes.json()
]); ]);
// Handle both old and new API response formats // Handle both old and new API response formats
@ -80,6 +84,15 @@ export default function Home() {
setStreams(streams); setStreams(streams);
setActiveSources(activeSources); setActiveSources(activeSources);
setCurrentScene(sceneName); 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) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.'); showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.');
@ -126,9 +139,16 @@ export default function Home() {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
// Update local state immediately for responsive UI // Update local state based on studio mode
setCurrentScene(sceneName); if (result.data.studioMode) {
showSuccess('Scene Changed', `Switched to ${sceneName} layout`); // 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 { } else {
throw new Error(result.error || 'Failed to switch scene'); throw new Error(result.error || 'Failed to switch scene');
} }
@ -138,6 +158,55 @@ export default function Home() {
} }
}, [showSuccess, showError]); }, [showSuccess, showError]);
// 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}`,
background: 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
};
} else if (isProgram) {
return {
isActive: true,
text: `Program: ${sceneName}`,
background: 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
};
} else if (isPreview) {
return {
isActive: true,
text: `Preview: ${sceneName}`,
background: 'linear-gradient(135deg, var(--solarized-yellow), var(--solarized-orange))'
};
} else {
return {
isActive: false,
text: `Set Preview: ${sceneName}`,
background: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
};
}
} else {
// Normal mode
if (isProgram) {
return {
isActive: true,
text: `Active: ${sceneName}`,
background: 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
};
} else {
return {
isActive: false,
text: `Switch to ${sceneName}`,
background: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
};
}
}
}, [currentScene, currentPreviewScene, studioModeEnabled]);
// Memoized corner displays to prevent re-renders // Memoized corner displays to prevent re-renders
const cornerDisplays = useMemo(() => [ const cornerDisplays = useMemo(() => [
{ screen: 'top_left' as const, label: 'Top Left' }, { screen: 'top_left' as const, label: 'Top Left' },
@ -182,17 +251,18 @@ export default function Home() {
<div className="glass p-6 mb-6"> <div className="glass p-6 mb-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="card-title mb-0">Primary Display</h2> <h2 className="card-title mb-0">Primary Display</h2>
<button {(() => {
onClick={() => handleSceneSwitch('1-Screen')} const buttonState = getSceneButtonState('1-Screen');
className={`btn ${currentScene === '1-Screen' ? 'active' : ''}`} return (
style={{ <button
background: currentScene === '1-Screen' onClick={() => handleSceneSwitch('1-Screen')}
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))' className={`btn ${buttonState.isActive ? 'active' : ''}`}
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))' style={{ background: buttonState.background }}
}} >
> {buttonState.text}
{currentScene === '1-Screen' ? 'Active: 1-Screen' : 'Switch to 1-Screen'} </button>
</button> );
})()}
</div> </div>
<div className="max-w-md mx-auto"> <div className="max-w-md mx-auto">
<Dropdown <Dropdown
@ -210,17 +280,18 @@ export default function Home() {
<div className="glass p-6 mb-6"> <div className="glass p-6 mb-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="card-title mb-0">Side Displays</h2> <h2 className="card-title mb-0">Side Displays</h2>
<button {(() => {
onClick={() => handleSceneSwitch('2-Screen')} const buttonState = getSceneButtonState('2-Screen');
className={`btn ${currentScene === '2-Screen' ? 'active' : ''}`} return (
style={{ <button
background: currentScene === '2-Screen' onClick={() => handleSceneSwitch('2-Screen')}
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))' className={`btn ${buttonState.isActive ? 'active' : ''}`}
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))' style={{ background: buttonState.background }}
}} >
> {buttonState.text}
{currentScene === '2-Screen' ? 'Active: 2-Screen' : 'Switch to 2-Screen'} </button>
</button> );
})()}
</div> </div>
<div className="grid-2"> <div className="grid-2">
<div> <div>
@ -252,17 +323,18 @@ export default function Home() {
<div className="glass p-6"> <div className="glass p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="card-title mb-0">Corner Displays</h2> <h2 className="card-title mb-0">Corner Displays</h2>
<button {(() => {
onClick={() => handleSceneSwitch('4-Screen')} const buttonState = getSceneButtonState('4-Screen');
className={`btn ${currentScene === '4-Screen' ? 'active' : ''}`} return (
style={{ <button
background: currentScene === '4-Screen' onClick={() => handleSceneSwitch('4-Screen')}
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))' className={`btn ${buttonState.isActive ? 'active' : ''}`}
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))' style={{ background: buttonState.background }}
}} >
> {buttonState.text}
{currentScene === '4-Screen' ? 'Active: 4-Screen' : 'Switch to 4-Screen'} </button>
</button> );
})()}
</div> </div>
<div className="grid-4"> <div className="grid-4">
{cornerDisplays.map(({ screen, label }) => ( {cornerDisplays.map(({ screen, label }) => (

View file

@ -13,9 +13,11 @@ type OBSStatus = {
obsWebSocketVersion: string; obsWebSocketVersion: string;
}; };
currentScene?: string; currentScene?: string;
currentPreviewScene?: string;
sceneCount?: number; sceneCount?: number;
streaming?: boolean; streaming?: boolean;
recording?: boolean; recording?: boolean;
studioModeEnabled?: boolean;
error?: string; error?: string;
}; };
@ -96,7 +98,7 @@ export default function Footer() {
<div>{obsStatus.host}:{obsStatus.port}</div> <div>{obsStatus.host}:{obsStatus.port}</div>
{obsStatus.hasPassword && <div>🔒 Authenticated</div>} {obsStatus.hasPassword && <div>🔒 Authenticated</div>}
{/* Streaming/Recording Status */} {/* Streaming/Recording/Studio Mode Status */}
{obsStatus.connected && ( {obsStatus.connected && (
<div className="flex gap-4 mt-4"> <div className="flex gap-4 mt-4">
<div className={`flex items-center gap-2 ${obsStatus.streaming ? 'text-red-400' : 'opacity-60'}`}> <div className={`flex items-center gap-2 ${obsStatus.streaming ? 'text-red-400' : 'opacity-60'}`}>
@ -108,6 +110,11 @@ export default function Footer() {
<div className={`status-dot ${obsStatus.recording ? 'streaming' : 'idle'}`} style={{width: '8px', height: '8px'}}></div> <div className={`status-dot ${obsStatus.recording ? 'streaming' : 'idle'}`} style={{width: '8px', height: '8px'}}></div>
<span className="text-sm">{obsStatus.recording ? 'REC' : 'IDLE'}</span> <span className="text-sm">{obsStatus.recording ? 'REC' : 'IDLE'}</span>
</div> </div>
<div className={`flex items-center gap-2 ${obsStatus.studioModeEnabled ? 'text-yellow-400' : 'opacity-60'}`}>
<div className={`status-dot ${obsStatus.studioModeEnabled ? 'connected' : 'idle'}`} style={{width: '8px', height: '8px'}}></div>
<span className="text-sm">{obsStatus.studioModeEnabled ? 'STUDIO' : 'DIRECT'}</span>
</div>
</div> </div>
)} )}
</div> </div>
@ -138,11 +145,18 @@ export default function Footer() {
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
{obsStatus.currentScene && ( {obsStatus.currentScene && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>Scene:</span> <span>{obsStatus.studioModeEnabled ? 'Program:' : 'Scene:'}</span>
<span className="font-medium">{obsStatus.currentScene}</span> <span className="font-medium">{obsStatus.currentScene}</span>
</div> </div>
)} )}
{obsStatus.studioModeEnabled && obsStatus.currentPreviewScene && (
<div className="flex justify-between">
<span>Preview:</span>
<span className="font-medium text-yellow-400">{obsStatus.currentPreviewScene}</span>
</div>
)}
{obsStatus.sceneCount !== null && ( {obsStatus.sceneCount !== null && (
<div className="flex justify-between"> <div className="flex justify-between">
<span>Total Scenes:</span> <span>Total Scenes:</span>