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:
parent
e777b3d422
commit
07028b0792
3 changed files with 147 additions and 43 deletions
|
@ -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';
|
||||||
|
|
126
app/page.tsx
126
app/page.tsx
|
@ -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
|
||||||
|
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);
|
setCurrentScene(sceneName);
|
||||||
showSuccess('Scene Changed', `Switched to ${sceneName} layout`);
|
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>
|
||||||
|
{(() => {
|
||||||
|
const buttonState = getSceneButtonState('1-Screen');
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSceneSwitch('1-Screen')}
|
onClick={() => handleSceneSwitch('1-Screen')}
|
||||||
className={`btn ${currentScene === '1-Screen' ? 'active' : ''}`}
|
className={`btn ${buttonState.isActive ? 'active' : ''}`}
|
||||||
style={{
|
style={{ background: buttonState.background }}
|
||||||
background: currentScene === '1-Screen'
|
|
||||||
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
|
|
||||||
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentScene === '1-Screen' ? 'Active: 1-Screen' : 'Switch to 1-Screen'}
|
{buttonState.text}
|
||||||
</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>
|
||||||
|
{(() => {
|
||||||
|
const buttonState = getSceneButtonState('2-Screen');
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSceneSwitch('2-Screen')}
|
onClick={() => handleSceneSwitch('2-Screen')}
|
||||||
className={`btn ${currentScene === '2-Screen' ? 'active' : ''}`}
|
className={`btn ${buttonState.isActive ? 'active' : ''}`}
|
||||||
style={{
|
style={{ background: buttonState.background }}
|
||||||
background: currentScene === '2-Screen'
|
|
||||||
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
|
|
||||||
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentScene === '2-Screen' ? 'Active: 2-Screen' : 'Switch to 2-Screen'}
|
{buttonState.text}
|
||||||
</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>
|
||||||
|
{(() => {
|
||||||
|
const buttonState = getSceneButtonState('4-Screen');
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSceneSwitch('4-Screen')}
|
onClick={() => handleSceneSwitch('4-Screen')}
|
||||||
className={`btn ${currentScene === '4-Screen' ? 'active' : ''}`}
|
className={`btn ${buttonState.isActive ? 'active' : ''}`}
|
||||||
style={{
|
style={{ background: buttonState.background }}
|
||||||
background: currentScene === '4-Screen'
|
|
||||||
? 'linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow))'
|
|
||||||
: 'linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan))'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentScene === '4-Screen' ? 'Active: 4-Screen' : 'Switch to 4-Screen'}
|
{buttonState.text}
|
||||||
</button>
|
</button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-4">
|
<div className="grid-4">
|
||||||
{cornerDisplays.map(({ screen, label }) => (
|
{cornerDisplays.map(({ screen, label }) => (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue