Add OBS group management feature and documentation
- Add group_name column to teams table for mapping teams to OBS groups - Create API endpoints for group creation (/api/createGroup) and bulk sync (/api/syncGroups) - Update teams UI with group status display and creation buttons - Implement automatic group assignment when adding streams - Add comprehensive OBS setup documentation (docs/OBS_SETUP.md) - Fix team list spacing issue with explicit margins - Update OBS client with group management functions - Add database migration script for existing deployments 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c259f0d943
commit
5789986bb6
14 changed files with 540 additions and 144 deletions
|
@ -18,7 +18,7 @@ export async function apiCall(url: string, options: RequestInit = {}): Promise<R
|
|||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
};
|
||||
|
||||
// Add API key if available
|
||||
|
|
110
lib/obsClient.js
110
lib/obsClient.js
|
@ -120,48 +120,74 @@ async function addSourceToSwitcher(inputName, newSources) {
|
|||
}
|
||||
}
|
||||
|
||||
// async function addSourceToGroup(obs, teamName, obs_source_name, url) {
|
||||
// try {
|
||||
// // Step 1: Check if the group exists
|
||||
// const { scenes } = await obs.call('GetSceneList');
|
||||
// const groupExists = scenes.some((scene) => scene.sceneName === teamName);
|
||||
async function createGroupIfNotExists(groupName) {
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
|
||||
// Check if the group (scene) exists
|
||||
const { scenes } = await obsClient.call('GetSceneList');
|
||||
const groupExists = scenes.some((scene) => scene.sceneName === groupName);
|
||||
|
||||
// // Step 2: Create the group if it doesn't exist
|
||||
// if (!groupExists) {
|
||||
// console.log(`Group "${teamName}" does not exist. Creating it.`);
|
||||
// await obs.call('CreateScene', { sceneName: teamName });
|
||||
// } else {
|
||||
// console.log(`Group "${teamName}" already exists.`);
|
||||
// }
|
||||
if (!groupExists) {
|
||||
console.log(`Creating group "${groupName}"`);
|
||||
await obsClient.call('CreateScene', { sceneName: groupName });
|
||||
return { created: true, message: `Group "${groupName}" created successfully` };
|
||||
} else {
|
||||
console.log(`Group "${groupName}" already exists`);
|
||||
return { created: false, message: `Group "${groupName}" already exists` };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating group:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// // Step 3: Add the source to the group
|
||||
// console.log(`Adding source "${obs_source_name}" to group "${teamName}".`);
|
||||
// await obs.call('CreateInput', {
|
||||
// sceneName: teamName,
|
||||
// inputName: obs_source_name,
|
||||
// inputKind: 'browser_source',
|
||||
// inputSettings: {
|
||||
// width: 1600,
|
||||
// height: 900,
|
||||
// url,
|
||||
// control_audio: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Step 4: Enable "Control audio via OBS"
|
||||
// await obs.call('SetInputSettings', {
|
||||
// inputName: obs_source_name,
|
||||
// inputSettings: {
|
||||
// control_audio: true, // Enable audio control
|
||||
// },
|
||||
// overlay: true, // Keep existing settings and apply changes
|
||||
// });
|
||||
|
||||
// console.log(`Source "${obs_source_name}" successfully added to group "${teamName}".`);
|
||||
// } catch (error) {
|
||||
// console.error('Error adding source to group:', error.message);
|
||||
// }
|
||||
// }
|
||||
async function addSourceToGroup(groupName, sourceName, url) {
|
||||
try {
|
||||
const obsClient = await getOBSClient();
|
||||
|
||||
// Ensure group exists
|
||||
await createGroupIfNotExists(groupName);
|
||||
|
||||
// Check if source already exists in the group
|
||||
const { sceneItems } = await obsClient.call('GetSceneItemList', { sceneName: groupName });
|
||||
const sourceExists = sceneItems.some(item => item.sourceName === sourceName);
|
||||
|
||||
if (!sourceExists) {
|
||||
// Create the browser source in the group
|
||||
console.log(`Adding source "${sourceName}" to group "${groupName}"`);
|
||||
await obsClient.call('CreateInput', {
|
||||
sceneName: groupName,
|
||||
inputName: sourceName,
|
||||
inputKind: 'browser_source',
|
||||
inputSettings: {
|
||||
width: 1600,
|
||||
height: 900,
|
||||
url,
|
||||
control_audio: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Ensure audio control is enabled
|
||||
await obsClient.call('SetInputSettings', {
|
||||
inputName: sourceName,
|
||||
inputSettings: {
|
||||
control_audio: true,
|
||||
},
|
||||
overlay: true,
|
||||
});
|
||||
|
||||
console.log(`Source "${sourceName}" successfully added to group "${groupName}"`);
|
||||
return { success: true, message: `Source added to group successfully` };
|
||||
} else {
|
||||
console.log(`Source "${sourceName}" already exists in group "${groupName}"`);
|
||||
return { success: false, message: `Source already exists in group` };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding source to group:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Export all functions
|
||||
|
@ -171,5 +197,7 @@ module.exports = {
|
|||
disconnectFromOBS,
|
||||
addSourceToSwitcher,
|
||||
ensureConnected,
|
||||
getConnectionStatus
|
||||
getConnectionStatus,
|
||||
createGroupIfNotExists,
|
||||
addSourceToGroup
|
||||
};
|
|
@ -7,7 +7,7 @@ export function useDebounce<T extends (...args: any[]) => any>(
|
|||
callback: T,
|
||||
delay: number
|
||||
): T {
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
return useCallback((...args: Parameters<T>) => {
|
||||
if (timeoutRef.current) {
|
||||
|
@ -159,7 +159,7 @@ export function useSmartPolling(
|
|||
) {
|
||||
const isVisible = usePageVisibility();
|
||||
const callbackRef = useRef(callback);
|
||||
const intervalRef = useRef<NodeJS.Timeout>();
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Update callback ref
|
||||
React.useEffect(() => {
|
||||
|
|
|
@ -18,7 +18,15 @@ export function isValidUrl(url: string): boolean {
|
|||
}
|
||||
|
||||
export function isPositiveInteger(value: unknown): value is number {
|
||||
return Number.isInteger(value) && value > 0;
|
||||
return Number.isInteger(value) && Number(value) > 0;
|
||||
}
|
||||
|
||||
export function validateInteger(value: unknown): number | null {
|
||||
const num = Number(value);
|
||||
if (Number.isInteger(num) && num > 0) {
|
||||
return num;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// String sanitization
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue