diff --git a/CLAUDE.md b/CLAUDE.md index faca2d8..692e8f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a Next.js web application (branded as "Live Stream Manager") that controls multiple OBS Source Switchers. It provides a UI for managing live stream sources across different screen layouts (large, left, right, topLeft, topRight, bottomLeft, bottomRight) and communicates with OBS WebSocket API to control streaming sources. +This is a Next.js web application that controls multiple OBS Source Switchers. It provides a UI for managing stream sources across different screen layouts (large, left, right, topLeft, topRight, bottomLeft, bottomRight) and communicates with OBS WebSocket API to control streaming sources. ## Key Commands @@ -42,8 +42,6 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro - `useToast.ts` - Toast notification system for user feedback - `security.ts` - Input validation and sanitization utilities - `/types` - TypeScript type definitions - - `Stream`, `StreamWithTeam` - Stream data types with team relationships - - `Team` - Team data with group management fields - `/files` - Default directory for SQLite database and text files (configurable via .env.local) - `/scripts` - Database setup and management scripts - `/.forgejo/workflows` - Forgejo CI/CD workflows for self-hosted runners @@ -87,26 +85,17 @@ This is a Next.js web application (branded as "Live Stream Manager") that contro - `POST /api/addStream` - Add new stream to database and create browser source in OBS (accepts Twitch username, auto-generates URL) - `GET /api/streams` - Get all available streams - `GET /api/streams/[id]` - Get individual stream details -- `DELETE /api/streams/[id]` - Delete stream with comprehensive OBS cleanup: - - Removes stream's nested scene - - Deletes browser source - - Removes from all source switchers - - Clears text files referencing the stream +- `DELETE /api/streams/[id]` - Delete stream from both OBS and database with confirmation #### Source Control -- `POST /api/setActive` - Set active stream for specific screen position (writes team-prefixed stream name to text file) +- `POST /api/setActive` - Set active stream for specific screen position - `GET /api/getActive` - Get currently active sources for all screens #### Team Management - `GET /api/teams` - Get all teams with group information - `POST /api/teams` - Create new team - `PUT /api/teams/[id]` - Update team name, group_name, or group_uuid -- `DELETE /api/teams/[teamId]` - Delete team with comprehensive OBS cleanup: - - Deletes team scene/group - - Removes team text source - - Deletes all associated stream scenes - - Removes all browser sources - - Clears all text files +- `DELETE /api/teams/[id]` - Delete team and associated streams - `GET /api/getTeamName` - Get team name by ID - `POST /api/createGroup` - Create OBS group from team and store UUID - `POST /api/syncGroups` - Synchronize all teams with OBS groups @@ -197,22 +186,12 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio ### Stream Management - **Twitch Integration**: Simplified stream addition using just Twitch username (auto-generates full URL) -- **Enhanced Stream Deletion**: Comprehensive cleanup that removes: - - Stream's nested scene from OBS - - Browser source and any references - - Entries from all source switchers - - Text files referencing the stream -- **Audio Control**: Browser sources created with "Control Audio via OBS" enabled and auto-muted +- **Stream Deletion**: Safe deletion workflow with confirmation that removes from both OBS and database - **Visual Feedback**: Clear "View Stream" links with proper contrast for accessibility -- **Team Association**: Streams organized under teams with proper naming conventions +- **Team Association**: Streams can be organized under teams for better management ### Team & Group Management - **UUID-based Tracking**: Robust OBS group synchronization using scene UUIDs -- **Enhanced Team Deletion**: Comprehensive cleanup that removes: - - Team scene/group from OBS - - Shared team text source - - All associated stream scenes and sources - - All browser sources with team prefix - **Sync Verification**: Real-time verification of database-OBS group synchronization - **Conflict Resolution**: UI actions to resolve sync issues (missing groups, name changes) - **Visual Indicators**: Clear status indicators for group linking and sync problems @@ -233,17 +212,4 @@ See [OBS Setup Guide](./docs/OBS_SETUP.md) for detailed configuration instructio - **API Documentation**: Well-documented endpoints with clear parameter validation - **Migration Scripts**: Database migration tools for schema updates - **Security**: Input validation, sanitization, and secure API design -- **Testing**: Comprehensive error handling and edge case management - -## Known Issues - -### Text Centering Problem -- **Issue**: Team name text overlays are not properly centered horizontally in OBS -- **Current Behavior**: Text left edge positions at center point (960px) instead of text center -- **Attempted Solutions**: - - Various alignment properties (alignment: 5, boundsAlignment: 5) - - Manual position calculation based on text width - - Different bounds configurations - - Multiple transform approaches -- **Workaround**: Manually change "Positional Alignment" to "Center" in OBS UI -- **Status**: Unresolved - requires further investigation into OBS API behavior \ No newline at end of file +- **Testing**: Comprehensive error handling and edge case management \ No newline at end of file diff --git a/README.md b/README.md index 54f63af..9e47f56 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ -# Live Stream Manager +# OBS Source Switcher Plugin UI -A professional [Next.js](https://nextjs.org) web application for managing live streams and controlling multiple OBS [Source Switchers](https://obsproject.com/forum/resources/source-switcher.941/) with real-time WebSocket integration and modern glass morphism UI. +A professional [Next.js](https://nextjs.org) web application for controlling multiple OBS [Source Switchers](https://obsproject.com/forum/resources/source-switcher.941/) with real-time WebSocket integration and modern glass morphism UI. ## Features - **Multi-Screen Source Control**: Manage 7 different screen positions (large, left, right, and 4 corners) - **Real-time OBS Integration**: WebSocket connection with live status monitoring -- **Enhanced Stream Management**: Create, edit, and delete streams with comprehensive OBS cleanup -- **Team Organization**: Organize streams by teams with full CRUD operations and scene synchronization -- **Comprehensive Deletion**: Remove streams/teams with complete OBS component cleanup (scenes, sources, text files) -- **Audio Control**: Browser sources created with muted audio and OBS control enabled -- **Modern UI**: Glass morphism design with responsive layout and accessibility features +- **Team & Stream Management**: Organize streams by teams with full CRUD operations +- **Modern UI**: Glass morphism design with responsive layout - **Professional Broadcasting**: Audio routing, scene management, and live status indicators - **Dual Integration**: WebSocket API + text file monitoring for maximum compatibility -- **UUID-based Tracking**: Robust OBS group synchronization with rename-safe tracking ## Quick Start @@ -105,59 +101,12 @@ npm run type-check # TypeScript validation ## API Endpoints -### Stream Management -- `GET /api/streams` - List all streams with team information -- `GET /api/streams/[id]` - Get individual stream details -- `POST /api/addStream` - Create new stream with browser source and team association -- `PUT /api/streams/[id]` - Update stream information -- `DELETE /api/streams/[id]` - Delete stream with comprehensive OBS cleanup: - - Removes stream's nested scene - - Deletes browser source - - Removes from all source switchers - - Clears text files referencing the stream +- `GET /api/streams` - List all streams +- `POST /api/addStream` - Create new stream and OBS source +- `POST /api/setActive` - Set active stream for screen position +- `GET /api/obsStatus` - Real-time OBS connection status +- `GET /api/teams` - Team management -### Source Control -- `POST /api/setActive` - Set active stream for screen position (writes team-prefixed name to text file) -- `GET /api/getActive` - Get currently active sources for all screen positions - -### Team Management -- `GET /api/teams` - Get all teams with group information and sync status -- `POST /api/teams` - Create new team with optional OBS scene creation -- `PUT /api/teams/[teamId]` - Update team name, group_name, or group_uuid -- `DELETE /api/teams/[teamId]` - Delete team with comprehensive OBS cleanup: - - Deletes team scene/group - - Removes team text source - - Deletes all associated stream scenes - - Removes all browser sources with team prefix - - Clears all related text files -- `GET /api/getTeamName` - Get team name by ID - -### OBS Group/Scene Management -- `POST /api/createGroup` - Create OBS scene from team and store UUID -- `POST /api/syncGroups` - Synchronize all teams with OBS groups -- `GET /api/verifyGroups` - Verify database groups exist in OBS with UUID tracking - - Detects orphaned groups (excludes system scenes) - - Identifies name mismatches - - Shows sync status for all teams - -### System Status -- `GET /api/obsStatus` - Real-time OBS connection, streaming, and recording status - -### Authentication -All endpoints require API key authentication when `API_KEY` environment variable is set. - -See `CLAUDE.md` for detailed architecture documentation and implementation details. - -## Known Issues - -### Text Centering -- **Issue**: Team name text overlays position left edge at center instead of centering the text itself -- **Workaround**: Manually change "Positional Alignment" to "Center" in OBS UI -- **Status**: Under investigation - requires further research into OBS API behavior - -### System Scene Exclusion -Infrastructure scenes containing source switchers are excluded from orphaned group detection: -- 1-Screen, 2-Screen, 4-Screen, Starting, Ending, Audio, Movies -- Additional scenes can be added to the `SYSTEM_SCENES` array in `/app/api/verifyGroups/route.ts` +See `CLAUDE.md` for detailed architecture documentation. diff --git a/app/api/setActive/route.ts b/app/api/setActive/route.ts index 8ccf1ab..61fb3b8 100644 --- a/app/api/setActive/route.ts +++ b/app/api/setActive/route.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import { FILE_DIRECTORY } from '../../../config'; import { getDatabase } from '../../../lib/database'; -import { StreamWithTeam } from '@/types'; +import { Stream } from '@/types'; import { validateScreenInput } from '../../../lib/security'; import { TABLE_NAMES } from '../../../lib/constants'; @@ -27,11 +27,8 @@ export async function POST(request: NextRequest) { try { const db = await getDatabase(); - const stream: StreamWithTeam | undefined = await db.get( - `SELECT s.*, t.team_name, t.group_name - FROM ${TABLE_NAMES.STREAMS} s - LEFT JOIN ${TABLE_NAMES.TEAMS} t ON s.team_id = t.team_id - WHERE s.id = ?`, + const stream: Stream | undefined = await db.get( + `SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`, [id] ); @@ -41,11 +38,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Stream not found' }, { status: 400 }); } - // Construct proper stream group name with team prefix - const groupName = stream.group_name || stream.team_name; - const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = stream.name.toLowerCase().replace(/\s+/g, '_'); - const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; + // Use stream group name instead of individual obs_source_name + const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`; fs.writeFileSync(filePath, streamGroupName); return NextResponse.json({ message: `${screen} updated successfully.` }, { status: 200 }); } catch (error) { diff --git a/app/api/streams/[id]/route.ts b/app/api/streams/[id]/route.ts index 72642f1..1a8974e 100644 --- a/app/api/streams/[id]/route.ts +++ b/app/api/streams/[id]/route.ts @@ -1,7 +1,16 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase } from '../../../../lib/database'; import { TABLE_NAMES } from '../../../../lib/constants'; -import { deleteStreamComponents, clearTextFilesForStream } from '../../../../lib/obsClient'; +import { getOBSClient } from '../../../../lib/obsClient'; + +interface OBSInput { + inputName: string; + inputUuid: string; +} + +interface GetInputListResponse { + inputs: OBSInput[]; +} // GET single stream export async function GET( @@ -94,12 +103,9 @@ export async function DELETE( const resolvedParams = await params; const db = await getDatabase(); - // Check if stream exists and get team info + // Check if stream exists const existingStream = await db.get( - `SELECT s.*, t.team_name, t.group_name - FROM ${TABLE_NAMES.STREAMS} s - LEFT JOIN ${TABLE_NAMES.TEAMS} t ON s.team_id = t.team_id - WHERE s.id = ?`, + `SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`, [resolvedParams.id] ); @@ -110,38 +116,35 @@ export async function DELETE( ); } - // Try comprehensive OBS cleanup first - let obsCleanupResults = null; + // Try to delete from OBS first try { - if (existingStream.name && existingStream.team_name) { - const groupName = existingStream.group_name || existingStream.team_name; + const obs = await getOBSClient(); + console.log('OBS client obtained:', !!obs); + + if (obs && existingStream.obs_source_name) { + console.log(`Attempting to remove OBS source: ${existingStream.obs_source_name}`); - console.log(`Starting comprehensive OBS cleanup for stream: ${existingStream.name}`); - console.log(`Team: ${existingStream.team_name}, Group: ${groupName}`); + // Get the input UUID first + const response = await obs.call('GetInputList'); + const inputs = response as GetInputListResponse; + console.log(`Found ${inputs.inputs.length} inputs in OBS`); - // Perform comprehensive OBS deletion - obsCleanupResults = await deleteStreamComponents( - existingStream.name, - existingStream.team_name, - groupName - ); - - console.log('OBS cleanup results:', obsCleanupResults); - - // Clear text files that reference this stream - const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = existingStream.name.toLowerCase().replace(/\s+/g, '_'); - const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; - - const textFileResults = await clearTextFilesForStream(streamGroupName); - console.log('Text file cleanup results:', textFileResults); + const input = inputs.inputs.find((i: OBSInput) => i.inputName === existingStream.obs_source_name); + if (input) { + console.log(`Found input with UUID: ${input.inputUuid}`); + await obs.call('RemoveInput', { inputUuid: input.inputUuid }); + console.log(`Successfully removed OBS source: ${existingStream.obs_source_name}`); + } else { + console.log(`Input not found in OBS: ${existingStream.obs_source_name}`); + console.log('Available inputs:', inputs.inputs.map((i: OBSInput) => i.inputName)); + } } else { - console.log('Missing stream or team information for comprehensive cleanup'); + console.log('OBS client not available or no source name provided'); } } catch (obsError) { - console.error('Error during comprehensive OBS cleanup:', obsError); - // Continue with database deletion even if OBS cleanup fails + console.error('Error removing source from OBS:', obsError); + // Continue with database deletion even if OBS removal fails } // Delete stream from database @@ -151,8 +154,7 @@ export async function DELETE( ); return NextResponse.json({ - message: 'Stream deleted successfully', - cleanup: obsCleanupResults || 'OBS cleanup was not performed' + message: 'Stream deleted successfully' }); } catch (error) { console.error('Error deleting stream:', error); diff --git a/app/api/teams/[teamId]/route.ts b/app/api/teams/[teamId]/route.ts index 96dd077..74582f4 100644 --- a/app/api/teams/[teamId]/route.ts +++ b/app/api/teams/[teamId]/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from 'next/server'; import { getDatabase } from '@/lib/database'; import { TABLE_NAMES } from '@/lib/constants'; -import { deleteTeamComponents, deleteStreamComponents, clearTextFilesForStream } from '@/lib/obsClient'; export async function PUT( request: Request, @@ -66,78 +65,26 @@ export async function DELETE( const teamId = parseInt(teamIdParam); const db = await getDatabase(); - // First get the team and stream information before deletion - const team = await db.get( - `SELECT * FROM ${TABLE_NAMES.TEAMS} WHERE team_id = ?`, - [teamId] - ); - - if (!team) { - return NextResponse.json({ error: 'Team not found' }, { status: 404 }); - } - - // Get all streams for this team - const streams = await db.all( - `SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE team_id = ?`, - [teamId] - ); - - console.log(`Deleting team "${team.team_name}" with ${streams.length} streams`); - - // Try to clean up OBS components first - let obsCleanupResults = null; - try { - // Delete each stream's OBS components - for (const stream of streams) { - try { - const groupName = team.group_name || team.team_name; - console.log(`Deleting OBS components for stream "${stream.name}"`); - - // Delete stream components - await deleteStreamComponents(stream.name, team.team_name, groupName); - - // Clear any text files that reference this stream - const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = stream.name.toLowerCase().replace(/\s+/g, '_'); - const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; - await clearTextFilesForStream(streamGroupName); - } catch (streamError) { - console.error(`Error deleting stream "${stream.name}" OBS components:`, streamError); - } - } - - // Delete team-level OBS components - obsCleanupResults = await deleteTeamComponents(team.team_name, team.group_name); - console.log('Team OBS cleanup results:', obsCleanupResults); - - } catch (obsError) { - console.error('Error during OBS cleanup:', obsError); - // Continue with database deletion even if OBS cleanup fails - } - - // Now delete from database await db.run('BEGIN TRANSACTION'); try { - // Delete all streams for this team await db.run( `DELETE FROM ${TABLE_NAMES.STREAMS} WHERE team_id = ?`, [teamId] ); - // Delete the team const result = await db.run( `DELETE FROM ${TABLE_NAMES.TEAMS} WHERE team_id = ?`, [teamId] ); - await db.run('COMMIT'); + if (result.changes === 0) { + await db.run('ROLLBACK'); + return NextResponse.json({ error: 'Team not found' }, { status: 404 }); + } - return NextResponse.json({ - message: 'Team and all associated components deleted successfully', - deletedStreams: streams.length, - obsCleanup: obsCleanupResults || 'OBS cleanup was not performed' - }); + await db.run('COMMIT'); + return NextResponse.json({ message: 'Team and associated streams deleted successfully' }); } catch (error) { await db.run('ROLLBACK'); throw error; diff --git a/app/api/verifyGroups/route.ts b/app/api/verifyGroups/route.ts index 40b6df0..df210fb 100644 --- a/app/api/verifyGroups/route.ts +++ b/app/api/verifyGroups/route.ts @@ -3,18 +3,6 @@ import { getDatabase } from '../../../lib/database'; import { TABLE_NAMES } from '../../../lib/constants'; import { getOBSClient } from '../../../lib/obsClient'; -// System scenes that should not be considered orphaned -// These are infrastructure scenes that contain source switchers or other system components -const SYSTEM_SCENES: string[] = [ - '1-Screen', - '2-Screen', - '4-Screen', - 'Starting', - 'Ending', - 'Audio', - 'Movies' -]; - interface OBSScene { sceneName: string; sceneUuid: string; @@ -82,8 +70,7 @@ export async function GET() { missing_in_obs: verification.filter(team => !team.exists_in_obs), name_mismatches: verification.filter(team => team.name_changed), orphaned_in_obs: obsScenes.filter(scene => - !teams.some(team => team.group_uuid === scene.sceneUuid || team.group_name === scene.sceneName) && - !SYSTEM_SCENES.includes(scene.sceneName) + !teams.some(team => team.group_uuid === scene.sceneUuid || team.group_name === scene.sceneName) ).map(s => ({ name: s.sceneName, uuid: s.sceneUuid })) } }); diff --git a/app/layout.tsx b/app/layout.tsx index c3ab1bc..7358406 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,8 +5,8 @@ import { ErrorBoundary } from '@/components/ErrorBoundary'; import PerformanceDashboard from '@/components/PerformanceDashboard'; export const metadata = { - title: 'Live Stream Manager', - description: 'A tool to manage live stream sources dynamically', + title: 'OBS Source Switcher', + description: 'A tool to manage OBS sources dynamically', }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/app/page.tsx b/app/page.tsx index 0e3ff66..37f33fd 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -140,7 +140,7 @@ export default function Home() {
{/* Title */}
-

Live Stream Control Center

+

Stream Control Center

Manage your OBS sources across multiple screen positions

diff --git a/components/Footer.tsx b/components/Footer.tsx index c43a131..3f6e15b 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -141,7 +141,7 @@ export default function Footer() { - Live Stream Manager + OBS Stream Manager
diff --git a/components/Header.tsx b/components/Header.tsx index 44f8cc6..6f6a908 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -20,7 +20,7 @@ export default function Header() {
-

Live Stream Manager

+

OBS Stream Manager

Professional Control

diff --git a/lib/obsClient.js b/lib/obsClient.js index 9193964..d2f5872 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -351,34 +351,12 @@ async function createStreamGroup(groupName, streamName, teamName, url) { }, }); console.log(`Created browser source "${sourceName}" in nested scene`); - - // Mute the audio stream for the browser source - try { - await obsClient.call('SetInputMute', { - inputName: sourceName, - inputMuted: true - }); - console.log(`Muted audio for browser source "${sourceName}"`); - } catch (muteError) { - console.error(`Failed to mute audio for "${sourceName}":`, muteError.message); - } } else { // Add existing source to nested scene await obsClient.call('CreateSceneItem', { sceneName: streamGroupName, sourceName: sourceName }); - - // Ensure audio is muted for existing source too - try { - await obsClient.call('SetInputMute', { - inputName: sourceName, - inputMuted: true - }); - console.log(`Ensured audio is muted for existing browser source "${sourceName}"`); - } catch (muteError) { - console.error(`Failed to mute audio for existing source "${sourceName}":`, muteError.message); - } } // Add text source to nested scene @@ -401,83 +379,18 @@ async function createStreamGroup(groupName, streamName, teamName, url) { // Position the sources properly in the nested scene if (browserSourceItem && textSourceItem) { try { - // Position text overlay at top, then center horizontally + // Position text overlay at top-left of the browser source await obsClient.call('SetSceneItemTransform', { sceneName: streamGroupName, // In the nested scene sceneItemId: textSourceItem.sceneItemId, sceneItemTransform: { - positionX: 0, // Start at left - positionY: 10, // Keep at top + positionX: 10, + positionY: 10, scaleX: 1.0, - scaleY: 1.0, - alignment: 5 // Center alignment + scaleY: 1.0 } }); - // Apply center horizontally transform (like clicking "Center Horizontally" in OBS UI) - const { sceneItemTransform: currentTransform } = await obsClient.call('GetSceneItemTransform', { - sceneName: streamGroupName, - sceneItemId: textSourceItem.sceneItemId - }); - - console.log('Current text transform before centering:', JSON.stringify(currentTransform, null, 2)); - - // Get the actual scene dimensions - let sceneWidth = 1920; // Default assumption - let sceneHeight = 1080; - - try { - const sceneInfo = await obsClient.call('GetSceneItemList', { sceneName: streamGroupName }); - console.log(`Scene dimensions check for "${streamGroupName}":`, sceneInfo); - } catch (e) { - console.log('Could not get scene info:', e.message); - } - - // Manual positioning: Calculate where to place text so its center is at canvas center - const canvasWidth = sceneWidth; - const canvasCenter = canvasWidth / 2; - const textWidth = currentTransform.width || currentTransform.sourceWidth || 0; - - // Since we know the scene is bounded to 1600x900 from earlier logs, try that - const boundedWidth = 1600; - const boundedCenter = boundedWidth / 2; // 800 - const alternatePosition = boundedCenter - (textWidth / 2); - - console.log(`Manual centering calculation:`); - console.log(`- Scene/Canvas width: ${canvasWidth}`); - console.log(`- Canvas center: ${canvasCenter}`); - console.log(`- Text width: ${textWidth}`); - console.log(`- Position for 1920px canvas: ${canvasCenter - (textWidth / 2)}`); - console.log(`- Bounded scene width: ${boundedWidth}`); - console.log(`- Bounded center: ${boundedCenter}`); - console.log(`- Position for 1600px bounded scene: ${alternatePosition}`); - - // Set the position with left alignment (0) for predictable positioning - await obsClient.call('SetSceneItemTransform', { - sceneName: streamGroupName, - sceneItemId: textSourceItem.sceneItemId, - sceneItemTransform: { - positionX: alternatePosition, // Use 1600px scene width calculation - positionY: 10, // Keep at top - alignment: 0, // Left alignment for predictable positioning - rotation: currentTransform.rotation || 0, - scaleX: currentTransform.scaleX || 1, - scaleY: currentTransform.scaleY || 1, - cropBottom: currentTransform.cropBottom || 0, - cropLeft: currentTransform.cropLeft || 0, - cropRight: currentTransform.cropRight || 0, - cropTop: currentTransform.cropTop || 0, - cropToBounds: currentTransform.cropToBounds || false - } - }); - - // Log the final transform to verify - const { sceneItemTransform: finalTransform } = await obsClient.call('GetSceneItemTransform', { - sceneName: streamGroupName, - sceneItemId: textSourceItem.sceneItemId - }); - console.log('Final text transform after centering:', JSON.stringify(finalTransform, null, 2)); - console.log(`Stream sources positioned in nested scene "${streamGroupName}"`); } catch (positionError) { console.error('Failed to position sources:', positionError.message || positionError); @@ -531,281 +444,6 @@ async function createStreamGroup(groupName, streamName, teamName, url) { } } -// Comprehensive stream deletion function -async function deleteStreamComponents(streamName, teamName, groupName) { - try { - const obsClient = await getOBSClient(); - - const cleanGroupName = groupName.toLowerCase().replace(/\s+/g, '_'); - const cleanStreamName = streamName.toLowerCase().replace(/\s+/g, '_'); - const streamGroupName = `${cleanGroupName}_${cleanStreamName}_stream`; - const sourceName = `${cleanGroupName}_${cleanStreamName}`; - const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text'; - - console.log(`Starting comprehensive deletion for stream "${streamName}"`); - console.log(`Components to delete: scene="${streamGroupName}", source="${sourceName}"`); - - // 1. Remove stream group scene item from team scene (if it exists) - try { - const { sceneItems: teamSceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName }); - const streamGroupItem = teamSceneItems.find(item => item.sourceName === streamGroupName); - - if (streamGroupItem) { - await obsClient.call('RemoveSceneItem', { - sceneName: groupName, - sceneItemId: streamGroupItem.sceneItemId - }); - console.log(`Removed stream group "${streamGroupName}" from team scene "${groupName}"`); - } - } catch (error) { - console.log(`Team scene "${groupName}" not found or stream group not in it:`, error.message); - } - - // 2. Remove the nested scene (stream group) - try { - await obsClient.call('RemoveScene', { sceneName: streamGroupName }); - console.log(`Removed nested scene "${streamGroupName}"`); - } catch (error) { - console.log(`Nested scene "${streamGroupName}" not found:`, error.message); - } - - // 3. Remove the browser source (if it's not used elsewhere) - try { - const { inputs } = await obsClient.call('GetInputList'); - const browserSource = inputs.find(input => input.inputName === sourceName); - - if (browserSource) { - await obsClient.call('RemoveInput', { inputUuid: browserSource.inputUuid }); - console.log(`Removed browser source "${sourceName}"`); - } - } catch (error) { - console.log(`Browser source "${sourceName}" not found:`, error.message); - } - - // 4. Check if text source should be removed (only if no other streams from this team exist) - try { - // This would require checking if other streams from the same team exist - // For now, we'll leave the text source as it's shared across team streams - console.log(`Keeping shared text source "${textSourceName}" (shared across team streams)`); - } catch (error) { - console.log(`Error checking text source usage:`, error.message); - } - - // 5. Remove from all source switchers - const screens = [ - 'ss_large', - 'ss_left', - 'ss_right', - 'ss_top_left', - 'ss_top_right', - 'ss_bottom_left', - 'ss_bottom_right' - ]; - - for (const screen of screens) { - try { - await removeSourceFromSwitcher(screen, streamGroupName); - console.log(`Removed "${streamGroupName}" from ${screen}`); - } catch (error) { - console.log(`Error removing from ${screen}:`, error.message); - } - } - - console.log(`Comprehensive deletion completed for stream "${streamName}"`); - return { - success: true, - message: 'Stream components deleted successfully', - deletedComponents: { - streamGroupName, - sourceName, - removedFromSwitchers: screens.length - } - }; - } catch (error) { - console.error('Error in comprehensive stream deletion:', error.message); - throw error; - } -} - -// Helper function to remove source from source switcher -async function removeSourceFromSwitcher(switcherName, sourceName) { - try { - const obsClient = await getOBSClient(); - - // Get current source switcher options - const { inputSettings } = await obsClient.call('GetInputSettings', { inputName: switcherName }); - const currentSources = inputSettings.sources || []; - - // Filter out the source we want to remove - const updatedSources = currentSources.filter(source => source.value !== sourceName); - - // Update the source switcher if changes were made - if (updatedSources.length !== currentSources.length) { - await obsClient.call('SetInputSettings', { - inputName: switcherName, - inputSettings: { - ...inputSettings, - sources: updatedSources - } - }); - console.log(`Removed "${sourceName}" from ${switcherName} (${currentSources.length - updatedSources.length} instances)`); - } else { - console.log(`Source "${sourceName}" not found in ${switcherName}`); - } - } catch (error) { - console.error(`Error removing source from ${switcherName}:`, error.message); - throw error; - } -} - -// Function to clear text files that reference the deleted stream -async function clearTextFilesForStream(streamGroupName) { - const fs = require('fs'); - const path = require('path'); - - try { - const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files'); - const screens = [ - 'large', - 'left', - 'right', - 'topLeft', - 'topRight', - 'bottomLeft', - 'bottomRight' - ]; - - let clearedFiles = []; - - for (const screen of screens) { - try { - const filePath = path.join(FILE_DIRECTORY, `${screen}.txt`); - - // Check if file exists and read its content - if (fs.existsSync(filePath)) { - const currentContent = fs.readFileSync(filePath, 'utf8').trim(); - - // If the file contains the stream group name we're deleting, clear it - if (currentContent === streamGroupName) { - fs.writeFileSync(filePath, ''); - clearedFiles.push(screen); - console.log(`Cleared ${screen}.txt (was referencing deleted stream "${streamGroupName}")`); - } - } - } catch (error) { - console.log(`Error checking/clearing ${screen}.txt:`, error.message); - } - } - - return { - success: true, - clearedFiles, - message: `Cleared ${clearedFiles.length} text files that referenced the deleted stream` - }; - } catch (error) { - console.error('Error clearing text files:', error.message); - throw error; - } -} - - -// Comprehensive team deletion function -async function deleteTeamComponents(teamName, groupName) { - try { - const obsClient = await getOBSClient(); - - console.log(`Starting comprehensive deletion for team "${teamName}"`); - - // 1. Delete the team scene (group) - if (groupName) { - try { - await obsClient.call('RemoveScene', { sceneName: groupName }); - console.log(`Removed team scene "${groupName}"`); - } catch (error) { - console.log(`Team scene "${groupName}" not found or already deleted:`, error.message); - } - } - - // 2. Delete the team text source (shared across all team streams) - const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text'; - try { - const { inputs } = await obsClient.call('GetInputList'); - const textSource = inputs.find(input => input.inputName === textSourceName); - - if (textSource) { - await obsClient.call('RemoveInput', { inputUuid: textSource.inputUuid }); - console.log(`Removed team text source "${textSourceName}"`); - } - } catch (error) { - console.log(`Text source "${textSourceName}" not found:`, error.message); - } - - // 3. Get all scenes to check for nested stream scenes - try { - const { scenes } = await obsClient.call('GetSceneList'); - const cleanGroupName = (groupName || teamName).toLowerCase().replace(/\s+/g, '_'); - - // Find all nested stream scenes for this team - const streamScenes = scenes.filter(scene => - scene.sceneName.startsWith(`${cleanGroupName}_`) && - scene.sceneName.endsWith('_stream') - ); - - console.log(`Found ${streamScenes.length} stream scenes to delete`); - - // Delete each stream scene - for (const streamScene of streamScenes) { - try { - await obsClient.call('RemoveScene', { sceneName: streamScene.sceneName }); - console.log(`Removed stream scene "${streamScene.sceneName}"`); - } catch (error) { - console.log(`Error removing stream scene "${streamScene.sceneName}":`, error.message); - } - } - } catch (error) { - console.log(`Error finding stream scenes:`, error.message); - } - - // 4. Remove any browser sources associated with this team - try { - const { inputs } = await obsClient.call('GetInputList'); - const cleanGroupName = (groupName || teamName).toLowerCase().replace(/\s+/g, '_'); - - // Find all browser sources for this team - const teamBrowserSources = inputs.filter(input => - input.inputKind === 'browser_source' && - input.inputName.startsWith(`${cleanGroupName}_`) - ); - - console.log(`Found ${teamBrowserSources.length} browser sources to delete`); - - // Delete each browser source - for (const source of teamBrowserSources) { - try { - await obsClient.call('RemoveInput', { inputUuid: source.inputUuid }); - console.log(`Removed browser source "${source.inputName}"`); - } catch (error) { - console.log(`Error removing browser source "${source.inputName}":`, error.message); - } - } - } catch (error) { - console.log(`Error finding browser sources:`, error.message); - } - - console.log(`Comprehensive team deletion completed for "${teamName}"`); - return { - success: true, - message: 'Team components deleted successfully', - deletedComponents: { - teamScene: groupName, - textSource: textSourceName - } - }; - } catch (error) { - console.error('Error in comprehensive team deletion:', error.message); - throw error; - } -} // Export all functions module.exports = { @@ -819,9 +457,5 @@ module.exports = { addSourceToGroup, createTextSource, createStreamGroup, - getAvailableTextInputKind, - deleteStreamComponents, - removeSourceFromSwitcher, - clearTextFilesForStream, - deleteTeamComponents + getAvailableTextInputKind }; \ No newline at end of file diff --git a/types/index.ts b/types/index.ts index 7b615c3..8933587 100644 --- a/types/index.ts +++ b/types/index.ts @@ -5,11 +5,6 @@ export type Stream = { url: string; team_id: number; }; - -export type StreamWithTeam = Stream & { - team_name: string; - group_name?: string | null; - }; export type Screen = { screen: string;