obs-ss-plugin-webui/app/api/addStream/route.ts
Decobus caca548c45 Fix OBS group creation by using nested scenes
- Remove invalid CreateGroup API calls (not supported in OBS WebSocket v5)
- Implement nested scenes approach for stream grouping
- Create a separate scene for each stream containing browser source and text overlay
- Add nested scene to team scene to simulate group behavior
- Fix lint errors and remove unused imports

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 17:16:54 -04:00

213 lines
No EOL
6.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '../../../lib/database';
import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher, createStreamGroup } from '../../../lib/obsClient';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import path from 'path';
import { getTableName, BASE_TABLE_NAMES } from '../../../lib/constants';
interface OBSClient {
call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
}
interface OBSInput {
inputName: string;
}
interface GetInputListResponse {
inputs: OBSInput[];
}
const screens = [
'ss_large',
'ss_left',
'ss_right',
'ss_top_left',
'ss_top_right',
'ss_bottom_left',
'ss_bottom_right',
];
async function fetchTeamInfo(teamId: number) {
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files');
try {
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'
});
const teamInfo = await db.get(
`SELECT team_name, group_name, group_uuid FROM ${teamsTableName} WHERE team_id = ?`,
[teamId]
);
await db.close();
return teamInfo;
} catch (error) {
if (error instanceof Error) {
console.error('Error fetching team info:', error.message);
} else {
console.error('An unknown error occurred:', error);
}
return null;
}
}
import { validateStreamInput } from '../../../lib/security';
// Generate OBS source name from stream name
function generateOBSSourceName(streamName: string): string {
return streamName.toLowerCase().replace(/\s+/g, '_') + '_twitch';
}
export async function POST(request: NextRequest) {
let name: string, url: string, team_id: number, obs_source_name: string;
// Parse and validate request body
try {
const body = await request.json();
const validation = validateStreamInput(body);
if (!validation.valid) {
return NextResponse.json({
error: 'Validation failed',
details: validation.errors
}, { status: 400 });
}
({ name, url, team_id } = validation.data!);
// Auto-generate OBS source name from stream name
obs_source_name = generateOBSSourceName(name);
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
}
try {
// Connect to OBS WebSocket
console.log("Pre-connect")
await connectToOBS();
console.log('Pre client')
const obs: OBSClient = await getOBSClient();
// obs.on('message', (msg) => {
// console.log('Message from OBS:', msg);
// });
let inputs;
try {
const response = await obs.call('GetInputList');
const inputListResponse = response as unknown as GetInputListResponse;
inputs = inputListResponse.inputs;
// console.log('Inputs:', inputs);
} catch (err) {
if (err instanceof Error) {
console.error('Failed to fetch inputs:', err.message);
} else {
console.error('Failed to fetch inputs:', err);
}
throw new Error('GetInputList failed.');
}
const teamInfo = await fetchTeamInfo(team_id);
if (!teamInfo) {
throw new Error('Team not found');
}
console.log('Team Info:', teamInfo);
// Use group_name if it exists, otherwise use team_name
const groupName = teamInfo.group_name || teamInfo.team_name;
const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name);
if (!sourceExists) {
// Create stream group with text overlay
await createStreamGroup(groupName, name, teamInfo.team_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: { sceneName: string; sceneUuid: string }) => 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.`);
for (const screen of screens) {
try {
const streamGroupName = `${name.toLowerCase().replace(/\s+/g, '_')}_stream`;
await addSourceToSwitcher(screen, [
{ hidden: false, selected: false, value: streamGroupName },
]);
} catch (error) {
if (error instanceof Error) {
console.error(`Failed to add source to ${screen}:`, error.message);
} else {
console.error(`Failed to add source to ${screen}:`, error);
}
}
}
} else {
console.log(`OBS source "${obs_source_name}" already exists.`);
}
const db = await getDatabase();
const streamsTableName = getTableName(BASE_TABLE_NAMES.STREAMS, {
year: 2025,
season: 'summer',
suffix: 'sat'
});
const query = `INSERT INTO ${streamsTableName} (name, obs_source_name, url, team_id) VALUES (?, ?, ?, ?)`;
db.run(query, [name, obs_source_name, url, team_id])
await disconnectFromOBS();
return NextResponse.json({ message: 'Stream added successfully' }, {status: 201})
} catch (error) {
if (error instanceof Error) {
console.error('Error adding stream:', error.message);
} else {
console.error('An unknown error occurred while adding stream:', error);
}
await disconnectFromOBS();
return NextResponse.json({ error: 'Failed to add stream' }, { status: 500 });
}
}