From ae171fd96152d60d2d8652f65c08bba4e882506c Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 16:42:24 -0400 Subject: [PATCH 1/3] Fix status dot alignment in Footer component - Restructure connection status layout for better hierarchy - Move status dot to be inline with Connected/Disconnected text - Use proper flex alignment (items-center) for dot and text - Separate title from status indicator for cleaner UI - Maintain consistent spacing with gap-2 --- components/Footer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/Footer.tsx b/components/Footer.tsx index 9d2e03c..edbcb26 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -81,10 +81,10 @@ export default function Footer() {
{/* Connection Status */}
-
-
-
-

OBS Studio

+
+

OBS Studio

+
+

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

-- 2.49.0 From 612be2b227325e49d9669d694ac55e76ba5d9747 Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 16:46:45 -0400 Subject: [PATCH 2/3] Fix orphaned groups verification to include stream scenes - Add stream scene detection based on naming pattern: {team}_{stream}_stream - Query database for both teams and streams with proper joins - Generate expected stream scene names using cleanObsName logic - Filter orphaned scenes to exclude team scenes, stream scenes, and system scenes - Add expected_stream_scenes to response for debugging visibility - Improve orphaned detection accuracy for nested OBS scene structure --- app/api/verifyGroups/route.ts | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/app/api/verifyGroups/route.ts b/app/api/verifyGroups/route.ts index 1ed5515..d62aa94 100644 --- a/app/api/verifyGroups/route.ts +++ b/app/api/verifyGroups/route.ts @@ -27,9 +27,14 @@ interface GetSceneListResponse { export async function GET() { try { - // Get teams from database + // Get teams and streams from database const db = await getDatabase(); const teams = await db.all(`SELECT team_id, team_name, group_name, group_uuid FROM ${TABLE_NAMES.TEAMS} WHERE group_name IS NOT NULL OR group_uuid IS NOT NULL`); + const streams = await db.all(` + SELECT s.*, t.team_name, t.group_name as team_group_name + FROM ${TABLE_NAMES.STREAMS} s + LEFT JOIN ${TABLE_NAMES.TEAMS} t ON s.team_id = t.team_id + `); // Get scenes (groups) from OBS const obs = await getOBSClient(); @@ -37,6 +42,11 @@ export async function GET() { const obsData = response as GetSceneListResponse; const obsScenes = obsData.scenes; + // Helper function to clean OBS names (matching obsClient.js logic) + const cleanObsName = (name: string): string => { + return name.toLowerCase().replace(/\s+/g, '_'); + }; + // Compare database groups with OBS scenes using both UUID and name const verification = teams.map(team => { let exists_in_obs = false; @@ -75,17 +85,42 @@ export async function GET() { }; }); + // Generate expected stream scene names based on the database + const expectedStreamScenes = streams.map(stream => { + if (stream.team_group_name) { + const cleanGroupName = cleanObsName(stream.team_group_name); + const cleanStreamName = cleanObsName(stream.name); + return `${cleanGroupName}_${cleanStreamName}_stream`; + } + return null; + }).filter(Boolean); + + // Check for orphaned scenes - scenes that exist in OBS but aren't in our database + const orphanedScenes = obsScenes.filter(scene => { + // Check if it's a team scene + const isTeamScene = teams.some(team => + team.group_uuid === scene.sceneUuid || team.group_name === scene.sceneName + ); + + // Check if it's an expected stream scene + const isStreamScene = expectedStreamScenes.includes(scene.sceneName); + + // Check if it's a system scene + const isSystemScene = SYSTEM_SCENES.includes(scene.sceneName); + + // It's orphaned if it's none of the above + return !isTeamScene && !isStreamScene && !isSystemScene; + }); + return NextResponse.json({ success: true, data: { teams_with_groups: verification, obs_scenes: obsScenes.map(s => ({ name: s.sceneName, uuid: s.sceneUuid })), + expected_stream_scenes: expectedStreamScenes, 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) - ).map(s => ({ name: s.sceneName, uuid: s.sceneUuid })) + orphaned_in_obs: orphanedScenes.map(s => ({ name: s.sceneName, uuid: s.sceneUuid })) } }); -- 2.49.0 From a89493b89f484917458ce57c6231ace4b5e6543f Mon Sep 17 00:00:00 2001 From: Decobus Date: Tue, 22 Jul 2025 18:03:56 -0400 Subject: [PATCH 3/3] Fix browser source audio control for individual stream management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable "Control audio via OBS" checkbox on browser sources using reroute_audio: true - Remove redundant separate audio capture sources (_audio suffixed sources) - Browser sources now properly route audio through OBS for individual mute/unmute control - Preserve URL settings in all SetInputSettings calls to prevent URL clearing - Simplify audio management - no more duplicate audio sources cluttering OBS This allows users to mute/unmute individual Twitch streams directly in OBS without having to manually mute each stream in the browser or deal with separate audio sources. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/obsClient.js | 64 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/lib/obsClient.js b/lib/obsClient.js index 26dfbe4..1232244 100644 --- a/lib/obsClient.js +++ b/lib/obsClient.js @@ -392,6 +392,8 @@ async function createStreamGroup(groupName, streamName, teamName, url) { const browserSourceExists = inputs.some(input => input.inputName === sourceName); if (!browserSourceExists) { + // Create browser source with detailed audio control settings + console.log(`Creating browser source "${sourceName}" with URL: ${url}`); await obsClient.call('CreateInput', { sceneName: streamGroupName, // Create in the nested scene inputName: sourceName, @@ -400,20 +402,46 @@ async function createStreamGroup(groupName, streamName, teamName, url) { width: 1920, height: 1080, url, - control_audio: true, + control_audio: true, // Enable audio control so OBS can mute the browser source + restart_when_active: false, + shutdown: false, + // Try additional audio-related settings + reroute_audio: true, + audio_monitoring_type: 0 // Monitor Off }, }); - console.log(`Created browser source "${sourceName}" in nested scene`); + console.log(`Created browser source "${sourceName}" in nested scene with URL: ${url}`); - // Mute the audio stream for the browser source + // Apply additional settings after creation to ensure they stick + try { + await obsClient.call('SetInputSettings', { + inputName: sourceName, + inputSettings: { + width: 1920, + height: 1080, + url, // Make sure URL is preserved + control_audio: true, + reroute_audio: true, + restart_when_active: false, + shutdown: false, + audio_monitoring_type: 0 + }, + overlay: false // Don't overlay, replace all settings + }); + console.log(`Applied complete settings to "${sourceName}" with URL: ${url}`); + } catch (settingsError) { + console.error(`Failed to apply settings to "${sourceName}":`, settingsError.message); + } + + // Mute the browser source audio by default (can be unmuted individually in OBS) try { await obsClient.call('SetInputMute', { inputName: sourceName, inputMuted: true }); - console.log(`Muted audio for browser source "${sourceName}"`); + console.log(`Muted browser source audio for "${sourceName}" (can be unmuted in OBS for individual control)`); } catch (muteError) { - console.error(`Failed to mute audio for "${sourceName}":`, muteError.message); + console.error(`Failed to mute browser source audio for "${sourceName}":`, muteError.message); } } else { // Add existing source to nested scene @@ -421,16 +449,32 @@ async function createStreamGroup(groupName, streamName, teamName, url) { sceneName: streamGroupName, sourceName: sourceName }); + console.log(`Added existing browser source "${sourceName}" to nested scene`); - // Ensure audio is muted for existing source too + // Ensure existing browser source has audio control enabled and correct URL try { + await obsClient.call('SetInputSettings', { + inputName: sourceName, + inputSettings: { + width: 1920, + height: 1080, + url, // Update URL in case it changed + control_audio: true, + reroute_audio: true, + restart_when_active: false, + shutdown: false, + audio_monitoring_type: 0 + }, + overlay: false // Replace settings, don't overlay + }); + 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); + console.log(`Updated existing browser source "${sourceName}" with URL: ${url} and enabled audio control`); + } catch (settingsError) { + console.error(`Failed to update settings for existing source "${sourceName}":`, settingsError.message); } } @@ -614,7 +658,7 @@ async function deleteStreamComponents(streamName, teamName, groupName) { console.log(`Nested scene "${streamGroupName}" not found:`, error.message); } - // 3. Remove the browser source (if it's not used elsewhere) + // 3. Remove the browser source try { const { inputs } = await obsClient.call('GetInputList'); const browserSource = inputs.find(input => input.inputName === sourceName); -- 2.49.0