Implement team name text overlays with refactored group structure
- Add createTextSource function with automatic OBS text input detection - Implement createStreamGroup to create groups within team scenes instead of separate scenes - Add team name text overlays positioned at top-left of each stream - Refactor stream switching to use stream group names for cleaner organization - Update setActive API to write stream group names to files - Fix getActive API to return correct screen position data - Improve team UUID assignment when adding streams - Remove manage streams section from home page for cleaner UI - Add vertical spacing to streams list to match teams page - Support dynamic text input kinds (text_ft2_source_v2, text_gdiplus, etc.) This creates a much cleaner OBS structure with 10 team scenes containing grouped stream sources rather than 200+ individual stream scenes, while adding team name text overlays for better stream identification. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ece75cf2df
commit
78f4d325d8
11 changed files with 325 additions and 49 deletions
236
lib/obsClient.js
236
lib/obsClient.js
|
@ -99,12 +99,22 @@ async function addSourceToSwitcher(inputName, newSources) {
|
|||
|
||||
// Step 1: Get current input settings
|
||||
const { inputSettings } = await obsClient.call('GetInputSettings', { inputName });
|
||||
// console.log('Current Settings:', inputSettings);
|
||||
console.log('Current Settings for', inputName, ':', inputSettings);
|
||||
|
||||
// Step 2: Add new sources to the sources array
|
||||
const updatedSources = [...inputSettings.sources, ...newSources];
|
||||
// Step 2: Initialize sources array if it doesn't exist or is not an array
|
||||
let currentSources = [];
|
||||
if (Array.isArray(inputSettings.sources)) {
|
||||
currentSources = inputSettings.sources;
|
||||
} else if (inputSettings.sources) {
|
||||
console.log('Sources is not an array, converting:', typeof inputSettings.sources);
|
||||
// Try to convert if it's an object or other format
|
||||
currentSources = [];
|
||||
}
|
||||
|
||||
// Step 3: Update the settings with the new sources array
|
||||
// Step 3: Add new sources to the sources array
|
||||
const updatedSources = [...currentSources, ...newSources];
|
||||
|
||||
// Step 4: Update the settings with the new sources array
|
||||
await obsClient.call('SetInputSettings', {
|
||||
inputName,
|
||||
inputSettings: {
|
||||
|
@ -202,6 +212,219 @@ async function addSourceToGroup(groupName, sourceName, url) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getAvailableTextInputKind() {
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
const { inputKinds } = await obsClient.call('GetInputKindList');
|
||||
|
||||
console.log('Available input kinds:', inputKinds);
|
||||
|
||||
// Check for text input kinds in order of preference
|
||||
const textKinds = ['text_gdiplus_v2', 'text_gdiplus', 'text_ft2_source_v2', 'text_ft2_source', 'text_source'];
|
||||
|
||||
for (const kind of textKinds) {
|
||||
if (inputKinds.includes(kind)) {
|
||||
console.log(`Found text input kind: ${kind}`);
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback - find any input kind that contains 'text'
|
||||
const textKind = inputKinds.find(kind => kind.toLowerCase().includes('text'));
|
||||
if (textKind) {
|
||||
console.log(`Found fallback text input kind: ${textKind}`);
|
||||
return textKind;
|
||||
}
|
||||
|
||||
throw new Error('No text input kind found');
|
||||
} catch (error) {
|
||||
console.error('Error getting available input kinds:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createTextSource(sceneName, textSourceName, text) {
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
|
||||
// Check if text source already exists globally in OBS
|
||||
const { inputs } = await obsClient.call('GetInputList');
|
||||
const existingInput = inputs.find(input => input.inputName === textSourceName);
|
||||
|
||||
if (!existingInput) {
|
||||
console.log(`Creating text source "${textSourceName}" in scene "${sceneName}"`);
|
||||
|
||||
// Get the correct text input kind for this OBS installation
|
||||
const inputKind = await getAvailableTextInputKind();
|
||||
|
||||
const inputSettings = {
|
||||
text,
|
||||
font: {
|
||||
face: 'Arial',
|
||||
size: 72,
|
||||
style: 'Bold'
|
||||
},
|
||||
color: 0xFFFFFFFF, // White text
|
||||
outline: true,
|
||||
outline_color: 0xFF000000, // Black outline
|
||||
outline_size: 4
|
||||
};
|
||||
|
||||
await obsClient.call('CreateInput', {
|
||||
sceneName,
|
||||
inputName: textSourceName,
|
||||
inputKind,
|
||||
inputSettings
|
||||
});
|
||||
|
||||
console.log(`Text source "${textSourceName}" created successfully with kind "${inputKind}"`);
|
||||
return { success: true, message: 'Text source created successfully' };
|
||||
} else {
|
||||
console.log(`Text source "${textSourceName}" already exists globally, updating settings`);
|
||||
|
||||
// Update existing text source settings
|
||||
const inputSettings = {
|
||||
text,
|
||||
font: {
|
||||
face: 'Arial',
|
||||
size: 72,
|
||||
style: 'Bold'
|
||||
},
|
||||
color: 0xFFFFFFFF, // White text
|
||||
outline: true,
|
||||
outline_color: 0xFF000000, // Black outline
|
||||
outline_size: 4
|
||||
};
|
||||
|
||||
await obsClient.call('SetInputSettings', {
|
||||
inputName: textSourceName,
|
||||
inputSettings
|
||||
});
|
||||
|
||||
console.log(`Text source "${textSourceName}" settings updated`);
|
||||
return { success: true, message: 'Text source settings updated' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating text source:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createStreamGroup(groupName, streamName, teamName, url) {
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
|
||||
// Ensure team scene exists
|
||||
await createGroupIfNotExists(groupName);
|
||||
|
||||
const streamGroupName = `${streamName.toLowerCase().replace(/\s+/g, '_')}_stream`;
|
||||
const sourceName = streamName.toLowerCase().replace(/\s+/g, '_') + '_twitch';
|
||||
const textSourceName = teamName.toLowerCase().replace(/\s+/g, '_') + '_text';
|
||||
|
||||
// Create text source globally (reused across streams in the team)
|
||||
await createTextSource(groupName, textSourceName, teamName);
|
||||
|
||||
// Create browser source directly in the team scene
|
||||
const { sceneItems: teamSceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName });
|
||||
const browserSourceExists = teamSceneItems.some(item => item.sourceName === sourceName);
|
||||
|
||||
if (!browserSourceExists) {
|
||||
await obsClient.call('CreateInput', {
|
||||
sceneName: groupName,
|
||||
inputName: sourceName,
|
||||
inputKind: 'browser_source',
|
||||
inputSettings: {
|
||||
width: 1600,
|
||||
height: 900,
|
||||
url,
|
||||
control_audio: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add text source to team scene if not already there
|
||||
const textInTeamScene = teamSceneItems.some(item => item.sourceName === textSourceName);
|
||||
if (!textInTeamScene) {
|
||||
await obsClient.call('CreateSceneItem', {
|
||||
sceneName: groupName,
|
||||
sourceName: textSourceName
|
||||
});
|
||||
}
|
||||
|
||||
// Get the scene items after adding sources
|
||||
const { sceneItems: updatedSceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName });
|
||||
|
||||
// Find the browser source and text source items
|
||||
const browserSourceItem = updatedSceneItems.find(item => item.sourceName === sourceName);
|
||||
const textSourceItem = updatedSceneItems.find(item => item.sourceName === textSourceName);
|
||||
|
||||
// Create a group within the team scene containing both sources
|
||||
if (browserSourceItem && textSourceItem) {
|
||||
try {
|
||||
// Create a group with both items
|
||||
await obsClient.call('CreateGroup', {
|
||||
sceneName: groupName,
|
||||
groupName: streamGroupName
|
||||
});
|
||||
|
||||
// Add both sources to the group
|
||||
await obsClient.call('SetSceneItemGroup', {
|
||||
sceneName: groupName,
|
||||
sceneItemId: browserSourceItem.sceneItemId,
|
||||
groupName: streamGroupName
|
||||
});
|
||||
|
||||
await obsClient.call('SetSceneItemGroup', {
|
||||
sceneName: groupName,
|
||||
sceneItemId: textSourceItem.sceneItemId,
|
||||
groupName: streamGroupName
|
||||
});
|
||||
|
||||
// Position text overlay at top-left within the group
|
||||
await obsClient.call('SetSceneItemTransform', {
|
||||
sceneName: groupName,
|
||||
sceneItemId: textSourceItem.sceneItemId,
|
||||
sceneItemTransform: {
|
||||
positionX: 10,
|
||||
positionY: 10,
|
||||
scaleX: 1.0,
|
||||
scaleY: 1.0
|
||||
}
|
||||
});
|
||||
|
||||
// Lock the group items
|
||||
await obsClient.call('SetSceneItemLocked', {
|
||||
sceneName: groupName,
|
||||
sceneItemId: browserSourceItem.sceneItemId,
|
||||
sceneItemLocked: true
|
||||
});
|
||||
|
||||
await obsClient.call('SetSceneItemLocked', {
|
||||
sceneName: groupName,
|
||||
sceneItemId: textSourceItem.sceneItemId,
|
||||
sceneItemLocked: true
|
||||
});
|
||||
|
||||
console.log(`Stream group "${streamGroupName}" created within team scene "${groupName}"`);
|
||||
} catch (groupError) {
|
||||
console.log('Group creation failed, sources added individually to team scene');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Stream sources added to team scene "${groupName}" with text overlay`);
|
||||
return {
|
||||
success: true,
|
||||
message: 'Stream group created within team scene',
|
||||
streamGroupName,
|
||||
sourceName,
|
||||
textSourceName
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating stream group:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Export all functions
|
||||
module.exports = {
|
||||
|
@ -212,5 +435,8 @@ module.exports = {
|
|||
ensureConnected,
|
||||
getConnectionStatus,
|
||||
createGroupIfNotExists,
|
||||
addSourceToGroup
|
||||
addSourceToGroup,
|
||||
createTextSource,
|
||||
createStreamGroup,
|
||||
getAvailableTextInputKind
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue