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
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getDatabase } from '../../../lib/database';
|
import { getDatabase } from '../../../lib/database';
|
||||||
import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, createGroupIfNotExists, addSourceToGroup } from '../../../lib/obsClient';
|
import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, createGroupIfNotExists, addSourceToGroup, createStreamGroup } from '../../../lib/obsClient';
|
||||||
import { open } from 'sqlite';
|
import { open } from 'sqlite';
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
@ -44,7 +44,7 @@ async function fetchTeamInfo(teamId: number) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const teamInfo = await db.get(
|
const teamInfo = await db.get(
|
||||||
`SELECT team_name, group_name FROM ${teamsTableName} WHERE team_id = ?`,
|
`SELECT team_name, group_name, group_uuid FROM ${teamsTableName} WHERE team_id = ?`,
|
||||||
[teamId]
|
[teamId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -130,16 +130,53 @@ export async function POST(request: NextRequest) {
|
||||||
const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name);
|
const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name);
|
||||||
|
|
||||||
if (!sourceExists) {
|
if (!sourceExists) {
|
||||||
// Create/ensure group exists and add source to it
|
// Create stream group with text overlay
|
||||||
await createGroupIfNotExists(groupName);
|
const result = await createStreamGroup(groupName, name, teamInfo.team_name, url);
|
||||||
await addSourceToGroup(groupName, obs_source_name, url);
|
|
||||||
|
// Update team with group UUID if not set
|
||||||
|
if (!teamInfo.group_uuid) {
|
||||||
|
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
|
||||||
|
const dbPath = path.join(FILE_DIRECTORY, 'sources.db');
|
||||||
|
const db = await open({
|
||||||
|
filename: dbPath,
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamsTableName = getTableName(BASE_TABLE_NAMES.TEAMS, {
|
||||||
|
year: 2025,
|
||||||
|
season: 'summer',
|
||||||
|
suffix: 'sat'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the scene UUID for the group
|
||||||
|
const obsClient = await getOBSClient();
|
||||||
|
const { scenes } = await obsClient.call('GetSceneList');
|
||||||
|
const scene = scenes.find((s: any) => s.sceneName === groupName);
|
||||||
|
|
||||||
|
if (scene) {
|
||||||
|
await db.run(
|
||||||
|
`UPDATE ${teamsTableName} SET group_name = ?, group_uuid = ? WHERE team_id = ?`,
|
||||||
|
[groupName, scene.sceneUuid, team_id]
|
||||||
|
);
|
||||||
|
console.log(`Updated team ${team_id} with group UUID: ${scene.sceneUuid}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Scene "${groupName}" not found in OBS`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating team group UUID:', error);
|
||||||
|
} finally {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`OBS source "${obs_source_name}" created.`);
|
console.log(`OBS source "${obs_source_name}" created.`);
|
||||||
|
|
||||||
for (const screen of screens) {
|
for (const screen of screens) {
|
||||||
try {
|
try {
|
||||||
|
const streamGroupName = `${name.toLowerCase().replace(/\s+/g, '_')}_stream`;
|
||||||
await addSourceToSwitcher(screen, [
|
await addSourceToSwitcher(screen, [
|
||||||
{ hidden: false, selected: false, value: obs_source_name },
|
{ hidden: false, selected: false, value: streamGroupName },
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
|
@ -47,8 +47,8 @@ export async function GET() {
|
||||||
const chicken = fs.existsSync(chickenPath) ? fs.readFileSync(chickenPath, 'utf-8') : null;
|
const chicken = fs.existsSync(chickenPath) ? fs.readFileSync(chickenPath, 'utf-8') : null;
|
||||||
|
|
||||||
// For SaT
|
// For SaT
|
||||||
// return NextResponse.json({ large, left, right, topLeft, topRight, bottomLeft, bottomRight }, {status: 201})
|
return NextResponse.json({ large, left, right, topLeft, topRight, bottomLeft, bottomRight }, {status: 201})
|
||||||
return NextResponse.json({ tank, tree, kitty, chicken }, {status: 201})
|
// return NextResponse.json({ tank, tree, kitty, chicken }, {status: 201})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading active sources:', error);
|
console.error('Error reading active sources:', error);
|
||||||
return NextResponse.json({ error: 'Failed to read active sources' }, {status: 500});
|
return NextResponse.json({ error: 'Failed to read active sources' }, {status: 500});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { FILE_DIRECTORY } from '../../../config';
|
||||||
import { getDatabase } from '../../../lib/database';
|
import { getDatabase } from '../../../lib/database';
|
||||||
import { Stream } from '@/types';
|
import { Stream } from '@/types';
|
||||||
import { validateScreenInput } from '../../../lib/security';
|
import { validateScreenInput } from '../../../lib/security';
|
||||||
|
import { TABLE_NAMES } from '../../../lib/constants';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
// Parse and validate request body
|
// Parse and validate request body
|
||||||
|
@ -27,7 +28,7 @@ export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const db = await getDatabase();
|
const db = await getDatabase();
|
||||||
const stream: Stream | undefined = await db.get<Stream>(
|
const stream: Stream | undefined = await db.get<Stream>(
|
||||||
'SELECT * FROM streams_2025_spring_adr WHERE id = ?',
|
`SELECT * FROM ${TABLE_NAMES.STREAMS} WHERE id = ?`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -37,7 +38,9 @@ export async function POST(request: NextRequest) {
|
||||||
return NextResponse.json({ error: 'Stream not found' }, { status: 400 });
|
return NextResponse.json({ error: 'Stream not found' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(filePath, stream.obs_source_name);
|
// 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 });
|
return NextResponse.json({ message: `${screen} updated successfully.` }, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating active source:', error);
|
console.error('Error updating active source:', error);
|
||||||
|
|
|
@ -8,11 +8,12 @@ import {
|
||||||
createDatabaseError,
|
createDatabaseError,
|
||||||
parseRequestBody
|
parseRequestBody
|
||||||
} from '@/lib/apiHelpers';
|
} from '@/lib/apiHelpers';
|
||||||
|
import { createGroupIfNotExists, createTextSource } from '@/lib/obsClient';
|
||||||
|
|
||||||
// Validation for team creation
|
// Validation for team creation
|
||||||
function validateTeamInput(data: unknown): {
|
function validateTeamInput(data: unknown): {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
data?: { team_name: string };
|
data?: { team_name: string; create_obs_group?: boolean };
|
||||||
errors?: Record<string, string>
|
errors?: Record<string, string>
|
||||||
} {
|
} {
|
||||||
const errors: Record<string, string> = {};
|
const errors: Record<string, string> = {};
|
||||||
|
@ -22,7 +23,7 @@ function validateTeamInput(data: unknown): {
|
||||||
return { valid: false, errors };
|
return { valid: false, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { team_name } = data as { team_name?: unknown };
|
const { team_name, create_obs_group } = data as { team_name?: unknown; create_obs_group?: unknown };
|
||||||
|
|
||||||
if (!team_name || typeof team_name !== 'string') {
|
if (!team_name || typeof team_name !== 'string') {
|
||||||
errors.team_name = 'Team name is required and must be a string';
|
errors.team_name = 'Team name is required and must be a string';
|
||||||
|
@ -38,7 +39,10 @@ function validateTeamInput(data: unknown): {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
data: { team_name: (team_name as string).trim() }
|
data: {
|
||||||
|
team_name: (team_name as string).trim(),
|
||||||
|
create_obs_group: create_obs_group === true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +64,7 @@ export const POST = withErrorHandling(async (request: Request) => {
|
||||||
return bodyResult.response;
|
return bodyResult.response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { team_name } = bodyResult.data;
|
const { team_name, create_obs_group } = bodyResult.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = await getDatabase();
|
const db = await getDatabase();
|
||||||
|
@ -78,16 +82,37 @@ export const POST = withErrorHandling(async (request: Request) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let groupName: string | null = null;
|
||||||
|
let groupUuid: string | null = null;
|
||||||
|
|
||||||
|
// Create OBS group and text source if requested
|
||||||
|
if (create_obs_group) {
|
||||||
|
try {
|
||||||
|
const obsResult = await createGroupIfNotExists(team_name);
|
||||||
|
groupName = team_name;
|
||||||
|
groupUuid = obsResult.sceneUuid;
|
||||||
|
|
||||||
|
// Create text source for the team
|
||||||
|
const textSourceName = team_name.toLowerCase().replace(/\s+/g, '_') + '_text';
|
||||||
|
await createTextSource(team_name, textSourceName, team_name);
|
||||||
|
|
||||||
|
console.log(`OBS group and text source created for team "${team_name}"`);
|
||||||
|
} catch (obsError) {
|
||||||
|
console.error('Error creating OBS group:', obsError);
|
||||||
|
// Continue with team creation even if OBS fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db.run(
|
const result = await db.run(
|
||||||
`INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`,
|
`INSERT INTO ${TABLE_NAMES.TEAMS} (team_name, group_name, group_uuid) VALUES (?, ?, ?)`,
|
||||||
[team_name]
|
[team_name, groupName, groupUuid]
|
||||||
);
|
);
|
||||||
|
|
||||||
const newTeam: Team = {
|
const newTeam: Team = {
|
||||||
team_id: result.lastID!,
|
team_id: result.lastID!,
|
||||||
team_name: team_name,
|
team_name: team_name,
|
||||||
group_name: null,
|
group_name: groupName,
|
||||||
group_uuid: null
|
group_uuid: groupUuid
|
||||||
};
|
};
|
||||||
|
|
||||||
return createSuccessResponse(newTeam, 201);
|
return createSuccessResponse(newTeam, 201);
|
||||||
|
|
30
app/page.tsx
30
app/page.tsx
|
@ -98,10 +98,15 @@ export default function Home() {
|
||||||
const handleSetActive = useCallback(async (screen: ScreenType, id: number | null) => {
|
const handleSetActive = useCallback(async (screen: ScreenType, id: number | null) => {
|
||||||
const selectedStream = streams.find((stream) => stream.id === id);
|
const selectedStream = streams.find((stream) => stream.id === id);
|
||||||
|
|
||||||
|
// Generate stream group name for optimistic updates
|
||||||
|
const streamGroupName = selectedStream
|
||||||
|
? `${selectedStream.name.toLowerCase().replace(/\s+/g, '_')}_stream`
|
||||||
|
: null;
|
||||||
|
|
||||||
// Update local state immediately for optimistic updates
|
// Update local state immediately for optimistic updates
|
||||||
setActiveSources((prev) => ({
|
setActiveSources((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[screen]: selectedStream?.obs_source_name || null,
|
[screen]: streamGroupName,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Debounced backend update
|
// Debounced backend update
|
||||||
|
@ -205,29 +210,6 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Manage Streams Section */}
|
|
||||||
{streams.length > 0 && (
|
|
||||||
<div className="glass p-6 mt-6">
|
|
||||||
<h2 className="card-title">Manage Streams</h2>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{streams.map((stream) => (
|
|
||||||
<div key={stream.id} className="glass p-4 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-white">{stream.name}</h3>
|
|
||||||
<p className="text-sm text-white/60">{stream.obs_source_name}</p>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={`/edit/${stream.id}`}
|
|
||||||
className="btn-secondary btn-sm"
|
|
||||||
>
|
|
||||||
<span className="icon">✏️</span>
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Toast Notifications */}
|
{/* Toast Notifications */}
|
||||||
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
||||||
|
|
|
@ -289,7 +289,7 @@ export default function AddStream() {
|
||||||
{streams.map((stream) => {
|
{streams.map((stream) => {
|
||||||
const team = teams.find(t => t.id === stream.team_id);
|
const team = teams.find(t => t.id === stream.team_id);
|
||||||
return (
|
return (
|
||||||
<div key={stream.id} className="glass p-4">
|
<div key={stream.id} className="glass p-4 mb-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div
|
<div
|
||||||
|
|
1
files/ss_large.txt
Normal file
1
files/ss_large.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
wa_stream
|
0
files/ss_left.txt
Normal file
0
files/ss_left.txt
Normal file
0
files/ss_right.text
Normal file
0
files/ss_right.text
Normal file
236
lib/obsClient.js
236
lib/obsClient.js
|
@ -99,12 +99,22 @@ async function addSourceToSwitcher(inputName, newSources) {
|
||||||
|
|
||||||
// Step 1: Get current input settings
|
// Step 1: Get current input settings
|
||||||
const { inputSettings } = await obsClient.call('GetInputSettings', { inputName });
|
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
|
// Step 2: Initialize sources array if it doesn't exist or is not an array
|
||||||
const updatedSources = [...inputSettings.sources, ...newSources];
|
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', {
|
await obsClient.call('SetInputSettings', {
|
||||||
inputName,
|
inputName,
|
||||||
inputSettings: {
|
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
|
// Export all functions
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -212,5 +435,8 @@ module.exports = {
|
||||||
ensureConnected,
|
ensureConnected,
|
||||||
getConnectionStatus,
|
getConnectionStatus,
|
||||||
createGroupIfNotExists,
|
createGroupIfNotExists,
|
||||||
addSourceToGroup
|
addSourceToGroup,
|
||||||
|
createTextSource,
|
||||||
|
createStreamGroup,
|
||||||
|
getAvailableTextInputKind
|
||||||
};
|
};
|
|
@ -43,7 +43,9 @@ export function createStreamLookupMaps(streams: Array<{ id: number; obs_source_n
|
||||||
const idToStreamMap = new Map<number, { id: number; obs_source_name: string; name: string }>();
|
const idToStreamMap = new Map<number, { id: number; obs_source_name: string; name: string }>();
|
||||||
|
|
||||||
streams.forEach(stream => {
|
streams.forEach(stream => {
|
||||||
sourceToIdMap.set(stream.obs_source_name, stream.id);
|
// Generate stream group name to match what's written to files
|
||||||
|
const streamGroupName = `${stream.name.toLowerCase().replace(/\s+/g, '_')}_stream`;
|
||||||
|
sourceToIdMap.set(streamGroupName, stream.id);
|
||||||
idToStreamMap.set(stream.id, stream);
|
idToStreamMap.set(stream.id, stream);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue