diff --git a/app/api/addStream/route.ts b/app/api/addStream/route.ts index 75a62a3..9347106 100644 --- a/app/api/addStream/route.ts +++ b/app/api/addStream/route.ts @@ -63,7 +63,7 @@ function generateOBSSourceName(teamSceneName: string, streamName: string): strin } export async function POST(request: NextRequest) { - let name: string, url: string, team_id: number, obs_source_name: string, lockSources: boolean; + let name: string, url: string, team_id: number, obs_source_name: string; // Parse and validate request body try { @@ -78,7 +78,6 @@ export async function POST(request: NextRequest) { } ({ name, url, team_id } = validation.data!); - lockSources = body.lockSources !== false; // Default to true if not specified } catch { return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); @@ -126,7 +125,7 @@ export async function POST(request: NextRequest) { if (!sourceExists) { // Create stream group with text overlay - await createStreamGroup(groupName, name, teamInfo.team_name, url, lockSources); + await createStreamGroup(groupName, name, teamInfo.team_name, url); // Update team with group UUID if not set if (!teamInfo.group_uuid) { diff --git a/app/api/obsStatus/route.ts b/app/api/obsStatus/route.ts index 91c85f6..13e8dc4 100644 --- a/app/api/obsStatus/route.ts +++ b/app/api/obsStatus/route.ts @@ -19,11 +19,9 @@ export async function GET() { obsWebSocketVersion: string; }; currentScene?: string; - currentPreviewScene?: string; sceneCount?: number; streaming?: boolean; recording?: boolean; - studioModeEnabled?: boolean; error?: string; } = { host: OBS_HOST, @@ -60,31 +58,15 @@ export async function GET() { // Get recording status 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.version = { obsVersion: versionInfo.obsVersion, obsWebSocketVersion: versionInfo.obsWebSocketVersion }; connectionStatus.currentScene = currentSceneInfo.sceneName; - connectionStatus.currentPreviewScene = currentPreviewScene; connectionStatus.sceneCount = sceneList.scenes.length; connectionStatus.streaming = streamStatus.outputActive; connectionStatus.recording = recordStatus.outputActive; - connectionStatus.studioModeEnabled = studioModeStatus.studioModeEnabled; } catch (err) { connectionStatus.error = err instanceof Error ? err.message : 'Unknown error occurred'; diff --git a/app/api/setScene/route.ts b/app/api/setScene/route.ts index 2363d32..548e003 100644 --- a/app/api/setScene/route.ts +++ b/app/api/setScene/route.ts @@ -32,30 +32,16 @@ export async function POST(request: NextRequest) { try { const obsClient = await getOBSClient(); - // Check if studio mode is active - const { studioModeEnabled } = await obsClient.call('GetStudioModeEnabled'); + // Switch to the requested scene + await obsClient.call('SetCurrentProgramScene', { sceneName }); - if (studioModeEnabled) { - // In studio mode, switch the preview scene - await obsClient.call('SetCurrentPreviewScene', { sceneName }); - console.log(`Successfully switched preview to scene: ${sceneName} (Studio Mode)`); - - return NextResponse.json({ - success: true, - data: { sceneName, studioMode: true }, - message: `Preview set to ${sceneName} layout (Studio Mode) - ready to transition` - }); - } else { - // Normal mode, switch program scene directly - await obsClient.call('SetCurrentProgramScene', { sceneName }); - console.log(`Successfully switched to scene: ${sceneName}`); - - return NextResponse.json({ - success: true, - data: { sceneName, studioMode: false }, - message: `Switched to ${sceneName} layout` - }); - } + console.log(`Successfully switched to scene: ${sceneName}`); + + return NextResponse.json({ + success: true, + data: { sceneName }, + message: `Switched to ${sceneName} layout` + }); } catch (obsError) { console.error('OBS WebSocket error:', obsError); return NextResponse.json( diff --git a/app/api/triggerTransition/route.ts b/app/api/triggerTransition/route.ts deleted file mode 100644 index 7136863..0000000 --- a/app/api/triggerTransition/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getOBSClient } from '../../../lib/obsClient'; - -export async function POST() { - try { - const obsClient = await getOBSClient(); - - // Check if studio mode is active - const { studioModeEnabled } = await obsClient.call('GetStudioModeEnabled'); - - if (!studioModeEnabled) { - return NextResponse.json( - { - success: false, - error: 'Studio mode is not enabled', - message: 'Studio mode must be enabled to trigger transitions' - }, - { status: 400 } - ); - } - - try { - // Trigger the studio mode transition (preview to program) - await obsClient.call('TriggerStudioModeTransition'); - console.log('Successfully triggered studio mode transition'); - - // Get the updated scene information after transition - const [programResponse, previewResponse] = await Promise.all([ - obsClient.call('GetCurrentProgramScene'), - obsClient.call('GetCurrentPreviewScene') - ]); - - return NextResponse.json({ - success: true, - data: { - programScene: programResponse.currentProgramSceneName, - previewScene: previewResponse.currentPreviewSceneName - }, - message: 'Successfully transitioned preview to program' - }); - } catch (obsError) { - console.error('OBS WebSocket error during transition:', obsError); - return NextResponse.json( - { - success: false, - error: 'Failed to trigger transition in OBS', - details: obsError instanceof Error ? obsError.message : 'Unknown error' - }, - { status: 500 } - ); - } - } catch (error) { - console.error('Error triggering transition:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to connect to OBS or trigger transition' - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index ae33ffe..e1107df 100644 --- a/app/globals.css +++ b/app/globals.css @@ -20,14 +20,6 @@ --solarized-red: #dc322f; --solarized-magenta: #d33682; --solarized-violet: #6c71c4; - - /* Gradient Custom Properties */ - --gradient-primary: linear-gradient(135deg, var(--solarized-blue), var(--solarized-cyan)); - --gradient-active: linear-gradient(135deg, var(--solarized-green), var(--solarized-yellow)); - --gradient-danger: linear-gradient(135deg, var(--solarized-red), var(--solarized-orange)); - --gradient-preview: linear-gradient(135deg, var(--solarized-yellow), var(--solarized-orange)); - --gradient-transition: linear-gradient(135deg, var(--solarized-red), var(--solarized-magenta)); - --gradient-body: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); } /* Modern CSS Foundation */ @@ -42,8 +34,8 @@ html { } body { - background: var(--gradient-body); - color: var(--solarized-base1); + background: linear-gradient(135deg, #002b36 0%, #073642 50%, #002b36 100%); + color: #93a1a1; min-height: 100vh; line-height: 1.6; } @@ -83,8 +75,8 @@ body { /* Modern Button System */ .btn { - background: var(--gradient-primary); - color: var(--solarized-base3); + background: linear-gradient(135deg, #268bd2, #2aa198); + color: #fdf6e3; border: none; padding: 12px 24px; border-radius: 12px; @@ -101,10 +93,9 @@ body { text-decoration: none; } -.btn.active, -.btn-success { - background: var(--gradient-active); - color: var(--solarized-base3); +.btn.active { + background: linear-gradient(135deg, #859900, #b58900); + color: #fdf6e3; box-shadow: 0 0 0 3px rgba(133, 153, 0, 0.5); transform: translateY(-1px); font-weight: 700; @@ -126,7 +117,7 @@ body { .btn-secondary { background: rgba(88, 110, 117, 0.3); border: 1px solid rgba(131, 148, 150, 0.4); - color: var(--solarized-base1); + color: #93a1a1; backdrop-filter: blur(10px); } @@ -136,14 +127,25 @@ body { box-shadow: 0 6px 20px rgba(88, 110, 117, 0.3); } +/* Success Button */ +.btn-success { + background: linear-gradient(135deg, #859900, #b58900); + color: #fdf6e3; +} + +.btn-success:hover { + background: linear-gradient(135deg, #b58900, #859900); + box-shadow: 0 6px 20px rgba(133, 153, 0, 0.4); +} + /* Danger Button */ .btn-danger { - background: var(--gradient-danger); - color: var(--solarized-base3); + background: linear-gradient(135deg, #dc322f, #cb4b16); + color: #fdf6e3; } .btn-danger:hover { - background: linear-gradient(135deg, var(--solarized-orange), var(--solarized-red)); + background: linear-gradient(135deg, #cb4b16, #dc322f); box-shadow: 0 6px 20px rgba(220, 50, 47, 0.4); } @@ -167,18 +169,6 @@ body { flex-shrink: 0; } -/* Scene Button Variants */ -.btn-scene-preview { - background: var(--gradient-preview); - color: var(--solarized-base3); -} - -.btn-scene-transition { - background: var(--gradient-transition); - color: var(--solarized-base3); - min-width: 120px; -} - /* Form spacing fixes since Tailwind gap classes aren't working */ .form-row { display: flex; @@ -204,7 +194,7 @@ body { border: 1px solid rgba(88, 110, 117, 0.4); border-radius: 12px; padding: 12px 16px; - color: var(--solarized-base1); + color: #93a1a1; width: 100%; transition: all 0.3s ease; } @@ -215,7 +205,7 @@ body { .input:focus { outline: none; - border-color: var(--solarized-blue); + border-color: #268bd2; box-shadow: 0 0 0 3px rgba(38, 139, 210, 0.2); } @@ -225,7 +215,7 @@ body { border: 1px solid rgba(88, 110, 117, 0.4); border-radius: 12px; padding: 12px 16px; - color: var(--solarized-base1); + color: #93a1a1; width: 100%; text-align: left; cursor: pointer; @@ -251,28 +241,6 @@ body { position: absolute; transform: translateZ(0); will-change: transform; - max-height: 400px; - overflow-y: auto; -} - -/* Custom scrollbar for dropdown */ -.dropdown-menu::-webkit-scrollbar { - width: 8px; -} - -.dropdown-menu::-webkit-scrollbar-track { - background: rgba(7, 54, 66, 0.5); - border-radius: 4px; -} - -.dropdown-menu::-webkit-scrollbar-thumb { - background: rgba(88, 110, 117, 0.6); - border-radius: 4px; - transition: background 0.2s ease; -} - -.dropdown-menu::-webkit-scrollbar-thumb:hover { - background: rgba(88, 110, 117, 0.8); } .dropdown-item { @@ -280,7 +248,7 @@ body { cursor: pointer; transition: all 0.2s ease; border-bottom: 1px solid rgba(88, 110, 117, 0.2); - color: var(--solarized-base1); + color: #93a1a1; } .dropdown-item:last-child { @@ -293,7 +261,7 @@ body { .dropdown-item.active { background: rgba(38, 139, 210, 0.3); - color: var(--solarized-base3); + color: #fdf6e3; } /* Icon Sizes */ @@ -388,22 +356,6 @@ body { margin-bottom: 32px; } -.ml-3 { - margin-left: 12px; -} - -.mr-1 { - margin-right: 4px; -} - -.mr-2 { - margin-right: 8px; -} - -.mr-4 { - margin-right: 16px; -} - .p-4 { padding: 16px; } @@ -414,79 +366,4 @@ body { .p-8 { padding: 32px; -} - -/* Collapsible Group Styles */ -.collapsible-group { - margin-bottom: 16px; -} - -.collapsible-header { - width: 100%; - background: rgba(7, 54, 66, 0.3); - border: 1px solid rgba(88, 110, 117, 0.3); - border-radius: 12px; - padding: 16px 20px; - cursor: pointer; - transition: all 0.3s ease; - margin-bottom: 1px; -} - -.collapsible-header:hover { - background: rgba(7, 54, 66, 0.5); - border-color: rgba(131, 148, 150, 0.4); -} - -.collapsible-header-content { - display: flex; - align-items: center; - gap: 12px; -} - -.collapsible-icon { - flex-shrink: 0; - transition: transform 0.3s ease; - color: var(--solarized-base1); -} - -.collapsible-icon.open { - transform: rotate(90deg); -} - -.collapsible-title { - flex: 1; - font-size: 18px; - font-weight: 600; - color: var(--solarized-base3); - text-align: left; - margin: 0; -} - -.collapsible-count { - background: rgba(38, 139, 210, 0.2); - color: var(--solarized-blue); - padding: 4px 12px; - border-radius: 20px; - font-size: 14px; - font-weight: 600; -} - -.collapsible-content { - overflow: hidden; - transition: all 0.3s ease; - max-height: 0; - opacity: 0; -} - -.collapsible-content.open { - max-height: 5000px; - opacity: 1; - margin-top: 12px; -} - -.collapsible-content-inner { - padding: 20px; - background: rgba(7, 54, 66, 0.2); - border: 1px solid rgba(88, 110, 117, 0.2); - border-radius: 12px; } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 3ed4197..7094cd5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -19,8 +19,6 @@ export default function Home() { 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 @@ -61,19 +59,17 @@ export default function Home() { 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 streams, active sources, and current scene in parallel + const [streamsRes, activeRes, sceneRes] = await Promise.all([ fetch('/api/streams'), fetch('/api/getActive'), - fetch('/api/getCurrentScene'), - fetch('/api/obsStatus') + fetch('/api/getCurrentScene') ]); - const [streamsData, activeData, sceneData, obsStatusData] = await Promise.all([ + const [streamsData, activeData, sceneData] = await Promise.all([ streamsRes.json(), activeRes.json(), - sceneRes.json(), - obsStatusRes.json() + sceneRes.json() ]); // Handle both old and new API response formats @@ -84,15 +80,6 @@ export default function Home() { 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.'); @@ -139,16 +126,9 @@ export default function Home() { 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`); - } + // Update local state immediately for responsive UI + setCurrentScene(sceneName); + showSuccess('Scene Changed', `Switched to ${sceneName} layout`); } else { throw new Error(result.error || 'Failed to switch scene'); } @@ -158,86 +138,6 @@ export default function Home() { } }, [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' }, @@ -282,29 +182,17 @@ export default function Home() {

Primary Display

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

Side Displays

-
- {(() => { - const buttonState = getSceneButtonState('2-Screen'); - return ( - <> - - {buttonState.showTransition && ( - - )} - - ); - })()} -
+
@@ -376,29 +252,17 @@ export default function Home() {

Corner Displays

-
- {(() => { - const buttonState = getSceneButtonState('4-Screen'); - return ( - <> - - {buttonState.showTransition && ( - - )} - - ); - })()} -
+
{cornerDisplays.map(({ screen, label }) => ( diff --git a/app/streams/page.tsx b/app/streams/page.tsx index fb1896c..2e4f52f 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -1,8 +1,7 @@ 'use client'; -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import Dropdown from '@/components/Dropdown'; -import CollapsibleGroup from '@/components/CollapsibleGroup'; import { Team } from '@/types'; import { useToast } from '@/lib/useToast'; import { ToastContainer } from '@/components/Toast'; @@ -15,172 +14,6 @@ interface Stream { team_id: number; } -interface StreamsByTeamProps { - streams: Stream[]; - teams: {id: number; name: string}[]; - onDelete: (stream: Stream) => void; -} - -function StreamsByTeam({ streams, teams, onDelete }: StreamsByTeamProps) { - const [expandedGroups, setExpandedGroups] = useState>(new Set()); - const [useCustomExpanded, setUseCustomExpanded] = useState(false); - - // Group streams by team - const streamsByTeam = useMemo(() => { - const grouped = new Map(); - - // Initialize with all teams - teams.forEach(team => { - grouped.set(team.id, []); - }); - - // Add "No Team" group for streams without a team - grouped.set(-1, []); - - // Group streams - streams.forEach(stream => { - const teamId = stream.team_id || -1; - const teamStreams = grouped.get(teamId) || []; - teamStreams.push(stream); - grouped.set(teamId, teamStreams); - }); - - // Only include groups that have streams - const result: Array<{teamId: number; teamName: string; streams: Stream[]}> = []; - - grouped.forEach((streamList, teamId) => { - if (streamList.length > 0) { - const team = teams.find(t => t.id === teamId); - result.push({ - teamId, - teamName: teamId === -1 ? 'No Team' : (team?.name || 'Unknown Team'), - streams: streamList - }); - } - }); - - // Sort by team name, with "No Team" at the end - result.sort((a, b) => { - if (a.teamId === -1) return 1; - if (b.teamId === -1) return -1; - return a.teamName.localeCompare(b.teamName); - }); - - return result; - }, [streams, teams]); - - const handleExpandAll = () => { - const allIds = streamsByTeam.map(group => group.teamId); - setExpandedGroups(new Set(allIds)); - setUseCustomExpanded(true); - }; - - const handleCollapseAll = () => { - setExpandedGroups(new Set()); - setUseCustomExpanded(true); - }; - - const handleToggleGroup = (teamId: number) => { - const newExpanded = new Set(expandedGroups); - if (newExpanded.has(teamId)) { - newExpanded.delete(teamId); - } else { - newExpanded.add(teamId); - } - setExpandedGroups(newExpanded); - setUseCustomExpanded(true); - }; - - return ( -
- {streamsByTeam.length > 0 && ( -
- - -
- )} -
- {streamsByTeam.map(({ teamId, teamName, streams: teamStreams }) => ( - handleToggleGroup(teamId)} - > -
- {teamStreams.map((stream) => ( -
-
-
-
- {stream.name.charAt(0).toUpperCase()} -
-
-
{stream.name}
-
OBS: {stream.obs_source_name}
-
-
-
-
ID: {stream.id}
-
- - - - - View Stream - - -
-
-
-
- ))} -
-
- ))} -
-
- ); -} - export default function AddStream() { const [formData, setFormData] = useState({ name: '', @@ -479,11 +312,61 @@ export default function AddStream() {
Create your first stream above!
) : ( - setDeleteConfirm({ id: stream.id, name: stream.name })} - /> +
+ {streams.map((stream) => { + const team = teams.find(t => t.id === stream.team_id); + return ( +
+
+
+
+ {stream.name.charAt(0).toUpperCase()} +
+
+
{stream.name}
+
OBS: {stream.obs_source_name}
+
Team: {team?.name || 'Unknown'}
+
+
+
+
ID: {stream.id}
+
+ + + + + View Stream + + +
+
+
+
+ ); + })} +
)}
diff --git a/components/CollapsibleGroup.tsx b/components/CollapsibleGroup.tsx deleted file mode 100644 index 0ea3c54..0000000 --- a/components/CollapsibleGroup.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import { useState, ReactNode } from 'react'; - -interface CollapsibleGroupProps { - title: string; - itemCount: number; - children: ReactNode; - defaultOpen?: boolean; - isOpen?: boolean; - onToggle?: () => void; -} - -export default function CollapsibleGroup({ - title, - itemCount, - children, - defaultOpen = true, - isOpen: controlledIsOpen, - onToggle -}: CollapsibleGroupProps) { - const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen); - - // Use controlled state if provided, otherwise use internal state - const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen; - - const handleToggle = () => { - if (onToggle) { - onToggle(); - } else { - setInternalIsOpen(!internalIsOpen); - } - }; - - return ( -
- - -
-
- {children} -
-
-
- ); -} \ No newline at end of file diff --git a/components/Footer.tsx b/components/Footer.tsx index d7c4f77..edbcb26 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -13,11 +13,9 @@ type OBSStatus = { obsWebSocketVersion: string; }; currentScene?: string; - currentPreviewScene?: string; sceneCount?: number; streaming?: boolean; recording?: boolean; - studioModeEnabled?: boolean; error?: string; }; @@ -85,8 +83,8 @@ export default function Footer() {

OBS Studio

-
-
+
+

{obsStatus?.connected ? 'Connected' : 'Disconnected'}

@@ -98,22 +96,17 @@ export default function Footer() {
{obsStatus.host}:{obsStatus.port}
{obsStatus.hasPassword &&
🔒 Authenticated
} - {/* Streaming/Recording/Studio Mode Status */} + {/* Streaming/Recording Status */} {obsStatus.connected && ( -
-
-
- {obsStatus.streaming ? 'LIVE' : 'OFFLINE'} +
+
+
+ {obsStatus.streaming ? 'LIVE' : 'OFFLINE'}
-
-
- {obsStatus.recording ? 'REC' : 'IDLE'} -
- -
-
- {obsStatus.studioModeEnabled ? 'STUDIO' : 'DIRECT'} +
+
+ {obsStatus.recording ? 'REC' : 'IDLE'}
)} @@ -145,18 +138,11 @@ export default function Footer() {
{obsStatus.currentScene && (
- {obsStatus.studioModeEnabled ? 'Program:' : 'Scene:'} + Scene: {obsStatus.currentScene}
)} - {obsStatus.studioModeEnabled && obsStatus.currentPreviewScene && ( -
- Preview: - {obsStatus.currentPreviewScene} -
- )} - {obsStatus.sceneCount !== null && (
Total Scenes: diff --git a/lib/obsClient.js b/lib/obsClient.js index 1aa6709..1232244 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -363,7 +363,7 @@ async function createTextSource(sceneName, textSourceName, text) { } } -async function createStreamGroup(groupName, streamName, teamName, url, lockSources = true) { +async function createStreamGroup(groupName, streamName, teamName, url) { try { const obsClient = await getOBSClient(); @@ -443,48 +443,14 @@ async function createStreamGroup(groupName, streamName, teamName, url, lockSourc } catch (muteError) { console.error(`Failed to mute browser source audio for "${sourceName}":`, muteError.message); } - - // Lock the newly created browser source if requested - if (lockSources) { - try { - // Get the scene items to find the browser source's ID - const { sceneItems } = await obsClient.call('GetSceneItemList', { sceneName: streamGroupName }); - const browserItem = sceneItems.find(item => item.sourceName === sourceName); - - if (browserItem) { - await obsClient.call('SetSceneItemLocked', { - sceneName: streamGroupName, - sceneItemId: browserItem.sceneItemId, - sceneItemLocked: true - }); - console.log(`Locked browser source "${sourceName}" in nested scene`); - } - } catch (lockError) { - console.error(`Failed to lock browser source "${sourceName}":`, lockError.message); - } - } } else { // Add existing source to nested scene - const { sceneItemId } = await obsClient.call('CreateSceneItem', { + await obsClient.call('CreateSceneItem', { sceneName: streamGroupName, sourceName: sourceName }); console.log(`Added existing browser source "${sourceName}" to nested scene`); - // Lock the scene item if requested - if (lockSources) { - try { - await obsClient.call('SetSceneItemLocked', { - sceneName: streamGroupName, - sceneItemId: sceneItemId, - sceneItemLocked: true - }); - console.log(`Locked browser source "${sourceName}" in nested scene`); - } catch (lockError) { - console.error(`Failed to lock browser source "${sourceName}":`, lockError.message); - } - } - // Ensure existing browser source has audio control enabled and correct URL try { await obsClient.call('SetInputSettings', { @@ -516,49 +482,21 @@ async function createStreamGroup(groupName, streamName, teamName, url, lockSourc const colorSourceName = `${textSourceName}_bg`; try { - const { sceneItemId: colorItemId } = await obsClient.call('CreateSceneItem', { + await obsClient.call('CreateSceneItem', { sceneName: streamGroupName, sourceName: colorSourceName }); console.log(`Added color source background "${colorSourceName}" to nested scene`); - - // Lock the color source if requested - if (lockSources) { - try { - await obsClient.call('SetSceneItemLocked', { - sceneName: streamGroupName, - sceneItemId: colorItemId, - sceneItemLocked: true - }); - console.log(`Locked color source background "${colorSourceName}"`); - } catch (lockError) { - console.error(`Failed to lock color source:`, lockError.message); - } - } } catch (error) { console.log('Color source background might already be in nested scene'); } try { - const { sceneItemId: textItemId } = await obsClient.call('CreateSceneItem', { + await obsClient.call('CreateSceneItem', { sceneName: streamGroupName, sourceName: textSourceName }); console.log(`Added text source "${textSourceName}" to nested scene`); - - // Lock the text source if requested - if (lockSources) { - try { - await obsClient.call('SetSceneItemLocked', { - sceneName: streamGroupName, - sceneItemId: textItemId, - sceneItemLocked: true - }); - console.log(`Locked text source "${textSourceName}"`); - } catch (lockError) { - console.error(`Failed to lock text source:`, lockError.message); - } - } } catch (error) { console.log('Text source might already be in nested scene'); } diff --git a/styles/Home.module.css b/styles/Home.module.css new file mode 100644 index 0000000..c890880 --- /dev/null +++ b/styles/Home.module.css @@ -0,0 +1,10 @@ +.linkButton { + padding: 10px 20px; + background: #0070f3; + color: #fff; + text-decoration: none; + border-radius: 5px; + display: inline-block; + text-align: center; + } + \ No newline at end of file