From d6c9ac8d7f734bd7ee80f8d96263bbe29196b2e9 Mon Sep 17 00:00:00 2001 From: Decobus Date: Sun, 20 Jul 2025 22:27:41 -0400 Subject: [PATCH] Major enhancements to stream management and UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed branding from "OBS Stream Manager" to "Live Stream Manager" throughout UI - Enhanced stream deletion with comprehensive OBS cleanup: - Removes stream's nested scene - Deletes browser source - Clears text files referencing the stream - Removes stream from all source switchers - Enhanced team deletion to clean up all OBS components: - Deletes team scene/group - Removes team text source - Deletes all associated stream scenes and sources - Clears all related text files - Fixed stream selection to use proper team-prefixed names in text files - Added StreamWithTeam type for proper team data handling - Improved browser source creation with audio controls: - Enabled "Control Audio via OBS" setting - Auto-mutes audio on creation - Attempted multiple approaches to fix text centering (still unresolved) Known issue: Text centering still positions left edge at center despite multiple attempts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/setActive/route.ts | 16 +- app/api/streams/[id]/route.ts | 68 +++--- app/api/teams/[teamId]/route.ts | 65 +++++- app/layout.tsx | 4 +- app/page.tsx | 2 +- components/Footer.tsx | 2 +- components/Header.tsx | 2 +- lib/obsClient.js | 376 +++++++++++++++++++++++++++++++- types/index.ts | 5 + 9 files changed, 484 insertions(+), 56 deletions(-) diff --git a/app/api/setActive/route.ts b/app/api/setActive/route.ts index 61fb3b8..8ccf1ab 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 { Stream } from '@/types'; +import { StreamWithTeam } from '@/types'; import { validateScreenInput } from '../../../lib/security'; import { TABLE_NAMES } from '../../../lib/constants'; @@ -27,8 +27,11 @@ export async function POST(request: NextRequest) { try { const db = await getDatabase(); - const stream: Stream | undefined = await db.get( - `SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`, + 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 = ?`, [id] ); @@ -38,8 +41,11 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Stream not found' }, { status: 400 }); } - // Use stream group name instead of individual obs_source_name - const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`; + // 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`; 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 1a8974e..72642f1 100644 --- a/app/api/streams/[id]/route.ts +++ b/app/api/streams/[id]/route.ts @@ -1,16 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase } from '../../../../lib/database'; import { TABLE_NAMES } from '../../../../lib/constants'; -import { getOBSClient } from '../../../../lib/obsClient'; - -interface OBSInput { - inputName: string; - inputUuid: string; -} - -interface GetInputListResponse { - inputs: OBSInput[]; -} +import { deleteStreamComponents, clearTextFilesForStream } from '../../../../lib/obsClient'; // GET single stream export async function GET( @@ -103,9 +94,12 @@ export async function DELETE( const resolvedParams = await params; const db = await getDatabase(); - // Check if stream exists + // Check if stream exists and get team info const existingStream = await db.get( - `SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`, + `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 = ?`, [resolvedParams.id] ); @@ -116,35 +110,38 @@ export async function DELETE( ); } - // Try to delete from OBS first + // Try comprehensive OBS cleanup first + let obsCleanupResults = null; try { - 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}`); + if (existingStream.name && existingStream.team_name) { + const groupName = existingStream.group_name || existingStream.team_name; - // 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`); + console.log(`Starting comprehensive OBS cleanup for stream: ${existingStream.name}`); + console.log(`Team: ${existingStream.team_name}, Group: ${groupName}`); - const input = inputs.inputs.find((i: OBSInput) => i.inputName === existingStream.obs_source_name); + // 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); - 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('OBS client not available or no source name provided'); + console.log('Missing stream or team information for comprehensive cleanup'); } } catch (obsError) { - console.error('Error removing source from OBS:', obsError); - // Continue with database deletion even if OBS removal fails + console.error('Error during comprehensive OBS cleanup:', obsError); + // Continue with database deletion even if OBS cleanup fails } // Delete stream from database @@ -154,7 +151,8 @@ export async function DELETE( ); return NextResponse.json({ - message: 'Stream deleted successfully' + message: 'Stream deleted successfully', + cleanup: obsCleanupResults || 'OBS cleanup was not performed' }); } 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 74582f4..96dd077 100644 --- a/app/api/teams/[teamId]/route.ts +++ b/app/api/teams/[teamId]/route.ts @@ -1,6 +1,7 @@ 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, @@ -65,26 +66,78 @@ 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] ); - if (result.changes === 0) { - await db.run('ROLLBACK'); - return NextResponse.json({ error: 'Team not found' }, { status: 404 }); - } - await db.run('COMMIT'); - return NextResponse.json({ message: 'Team and associated streams deleted successfully' }); + + return NextResponse.json({ + message: 'Team and all associated components deleted successfully', + deletedStreams: streams.length, + obsCleanup: obsCleanupResults || 'OBS cleanup was not performed' + }); } catch (error) { await db.run('ROLLBACK'); throw error; diff --git a/app/layout.tsx b/app/layout.tsx index 7358406..c3ab1bc 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: 'OBS Source Switcher', - description: 'A tool to manage OBS sources dynamically', + title: 'Live Stream Manager', + description: 'A tool to manage live stream sources dynamically', }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/app/page.tsx b/app/page.tsx index 37f33fd..0e3ff66 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -140,7 +140,7 @@ export default function Home() {
{/* Title */}
-

Stream Control Center

+

Live Stream Control Center

Manage your OBS sources across multiple screen positions

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

OBS Stream Manager

+

Live Stream Manager

Professional Control

diff --git a/lib/obsClient.js b/lib/obsClient.js index d2f5872..9193964 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -351,12 +351,34 @@ 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 @@ -379,18 +401,83 @@ async function createStreamGroup(groupName, streamName, teamName, url) { // Position the sources properly in the nested scene if (browserSourceItem && textSourceItem) { try { - // Position text overlay at top-left of the browser source + // Position text overlay at top, then center horizontally await obsClient.call('SetSceneItemTransform', { sceneName: streamGroupName, // In the nested scene sceneItemId: textSourceItem.sceneItemId, sceneItemTransform: { - positionX: 10, - positionY: 10, + positionX: 0, // Start at left + positionY: 10, // Keep at top scaleX: 1.0, - scaleY: 1.0 + scaleY: 1.0, + alignment: 5 // Center alignment } }); + // 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); @@ -444,6 +531,281 @@ 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 = { @@ -457,5 +819,9 @@ module.exports = { addSourceToGroup, createTextSource, createStreamGroup, - getAvailableTextInputKind + getAvailableTextInputKind, + deleteStreamComponents, + removeSourceFromSwitcher, + clearTextFilesForStream, + deleteTeamComponents }; \ No newline at end of file diff --git a/types/index.ts b/types/index.ts index 8933587..7b615c3 100644 --- a/types/index.ts +++ b/types/index.ts @@ -5,6 +5,11 @@ export type Stream = { url: string; team_id: number; }; + +export type StreamWithTeam = Stream & { + team_name: string; + group_name?: string | null; + }; export type Screen = { screen: string;