Initial commit - OBS Source Switcher Plugin UI
Complete Next.js application for managing OBS Source Switcher - Stream management with multiple screen layouts - Team management CRUD operations - SQLite database integration - OBS WebSocket API integration - Updated to latest versions (Next.js 15.4.1, React 19.1.0, Tailwind CSS 4.0.0) - Enhanced .gitignore for privacy and development
This commit is contained in:
commit
1d4b1eefba
43 changed files with 9596 additions and 0 deletions
196
app/api/addStream/route.ts
Normal file
196
app/api/addStream/route.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../lib/database';
|
||||
import { connectToOBS, getOBSClient, disconnectFromOBS, addSourceToSwitcher } from '../../../lib/obsClient';
|
||||
|
||||
interface OBSClient {
|
||||
call: (method: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
interface OBSScene {
|
||||
sceneName: string;
|
||||
}
|
||||
|
||||
interface OBSInput {
|
||||
inputName: string;
|
||||
}
|
||||
|
||||
interface GetSceneListResponse {
|
||||
currentProgramSceneName: string;
|
||||
currentPreviewSceneName: string;
|
||||
scenes: OBSScene[];
|
||||
}
|
||||
|
||||
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 fetchTeamName(teamId: number) {
|
||||
try {
|
||||
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
||||
const response = await fetch(`${baseUrl}/api/getTeamName?team_id=${teamId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch team name');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.team_name;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error:', error.message);
|
||||
} else {
|
||||
console.error('An unknown error occurred:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addBrowserSourceWithAudioControl(obs: OBSClient, sceneName: string, inputName: string, url: string) {
|
||||
try {
|
||||
// Step 1: Create the browser source input
|
||||
await obs.call('CreateInput', {
|
||||
sceneName,
|
||||
inputName,
|
||||
inputKind: 'browser_source',
|
||||
inputSettings: {
|
||||
width: 1600,
|
||||
height: 900,
|
||||
url,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Browser source "${inputName}" created successfully.`);
|
||||
|
||||
// Step 2: Wait for the input to initialize
|
||||
let inputReady = false;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
await obs.call('GetInputSettings', { inputName });
|
||||
inputReady = true;
|
||||
break;
|
||||
} catch {
|
||||
console.log(`Waiting for input "${inputName}" to initialize...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Wait 500ms before retrying
|
||||
}
|
||||
}
|
||||
|
||||
if (!inputReady) {
|
||||
throw new Error(`Input "${inputName}" did not initialize in time.`);
|
||||
}
|
||||
|
||||
|
||||
// Step 3: Enable "Reroute audio"
|
||||
await obs.call('SetInputSettings', {
|
||||
inputName,
|
||||
inputSettings: {
|
||||
reroute_audio: true,
|
||||
},
|
||||
overlay: true, // Keep existing settings and apply changes
|
||||
});
|
||||
|
||||
console.log(`Audio rerouted for "${inputName}".`);
|
||||
|
||||
// Step 4: Mute the input
|
||||
await obs.call('SetInputMute', {
|
||||
inputName,
|
||||
inputMuted: true,
|
||||
});
|
||||
|
||||
console.log(`Audio muted for "${inputName}".`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error adding browser source with audio control:', error.message);
|
||||
} else {
|
||||
console.error('An unknown error occurred while adding browser source:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, obs_source_name, url, team_id } = body;
|
||||
|
||||
if (!name || !obs_source_name || !url) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 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 teamName = await fetchTeamName(team_id);
|
||||
console.log('Team Name:', teamName)
|
||||
const response = await obs.call('GetSceneList');
|
||||
const sceneListResponse = response as unknown as GetSceneListResponse;
|
||||
const { scenes } = sceneListResponse;
|
||||
const groupExists = scenes.some((scene: OBSScene) => scene.sceneName === teamName);
|
||||
if (!groupExists) {
|
||||
await obs.call('CreateScene', { sceneName: teamName });
|
||||
}
|
||||
|
||||
const sourceExists = inputs.some((input: OBSInput) => input.inputName === obs_source_name);
|
||||
|
||||
if (!sourceExists) {
|
||||
await addBrowserSourceWithAudioControl(obs, teamName, obs_source_name, url)
|
||||
|
||||
console.log(`OBS source "${obs_source_name}" created.`);
|
||||
|
||||
for (const screen of screens) {
|
||||
try {
|
||||
await addSourceToSwitcher(screen, [
|
||||
{ hidden: false, selected: false, value: obs_source_name },
|
||||
]);
|
||||
} 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 query = `INSERT INTO streams_2025_spring_adr (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 });
|
||||
}
|
||||
}
|
57
app/api/getActive/route.ts
Normal file
57
app/api/getActive/route.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// import config from '../../../config';
|
||||
|
||||
const FILE_DIRECTORY = path.resolve(process.env.FILE_DIRECTORY || './files')
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(FILE_DIRECTORY)) {
|
||||
fs.mkdirSync(FILE_DIRECTORY, { recursive: true });
|
||||
}
|
||||
console.log('using', FILE_DIRECTORY)
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const largePath = path.join(FILE_DIRECTORY, 'large.txt');
|
||||
const leftPath = path.join(FILE_DIRECTORY, 'left.txt');
|
||||
const rightPath = path.join(FILE_DIRECTORY, 'right.txt');
|
||||
const topLeftPath = path.join(FILE_DIRECTORY, 'topLeft.txt');
|
||||
const topRightPath = path.join(FILE_DIRECTORY, 'topRight.txt');
|
||||
const bottomLeftPath = path.join(FILE_DIRECTORY, 'bottomLeft.txt');
|
||||
const bottomRightPath = path.join(FILE_DIRECTORY, 'bottomRight.txt');
|
||||
|
||||
const tankPath = path.join(FILE_DIRECTORY, 'tank.txt');
|
||||
const treePath = path.join(FILE_DIRECTORY, 'tree.txt');
|
||||
const kittyPath = path.join(FILE_DIRECTORY, 'kitty.txt');
|
||||
const chickenPath = path.join(FILE_DIRECTORY, 'chicken.txt');
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const large = fs.existsSync(largePath) ? fs.readFileSync(largePath, 'utf-8') : null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const left = fs.existsSync(leftPath) ? fs.readFileSync(leftPath, 'utf-8') : null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const right = fs.existsSync(rightPath) ? fs.readFileSync(rightPath, 'utf-8') : null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const topLeft = fs.existsSync(topLeftPath) ? fs.readFileSync(topLeftPath, 'utf-8') : null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const topRight = fs.existsSync(topRightPath) ? fs.readFileSync(topRightPath, 'utf-8') : null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const bottomLeft = fs.existsSync(bottomLeftPath) ? fs.readFileSync(bottomLeftPath, 'utf-8') : null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const bottomRight = fs.existsSync(bottomRightPath) ? fs.readFileSync(bottomRightPath, 'utf-8') : null;
|
||||
|
||||
const tank = fs.existsSync(tankPath) ? fs.readFileSync(tankPath, 'utf-8') : null;
|
||||
const tree = fs.existsSync(treePath) ? fs.readFileSync(treePath, 'utf-8') : null;
|
||||
const kitty = fs.existsSync(kittyPath) ? fs.readFileSync(kittyPath, 'utf-8') : null;
|
||||
const chicken = fs.existsSync(chickenPath) ? fs.readFileSync(chickenPath, 'utf-8') : null;
|
||||
|
||||
// For SaT
|
||||
// return NextResponse.json({ large, left, right, topLeft, topRight, bottomLeft, bottomRight }, {status: 201})
|
||||
return NextResponse.json({ tank, tree, kitty, chicken }, {status: 201})
|
||||
} catch (error) {
|
||||
console.error('Error reading active sources:', error);
|
||||
return NextResponse.json({ error: 'Failed to read active sources' }, {status: 500});
|
||||
}
|
||||
|
||||
}
|
38
app/api/getTeamName/route.ts
Normal file
38
app/api/getTeamName/route.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../lib/database';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Extract the team_id from the query string
|
||||
const { searchParams } = new URL(request.url);
|
||||
const teamId = searchParams.get('team_id');
|
||||
|
||||
if (!teamId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing team_id' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
const team = await db.get(
|
||||
'SELECT team_name FROM teams_2025_spring_adr WHERE team_id = ?',
|
||||
[teamId]
|
||||
);
|
||||
|
||||
if (!team) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Team not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ team_name: team.team_name });
|
||||
} catch (error) {
|
||||
console.error('Error fetching team name:', error instanceof Error ? error.message : String(error));
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch team name' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
43
app/api/setActive/route.ts
Normal file
43
app/api/setActive/route.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { FILE_DIRECTORY } from '../../../config';
|
||||
import { getDatabase } from '../../../lib/database';
|
||||
import { Stream, Screen } from '@/types';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body: Screen = await request.json();
|
||||
const { screen, id } = body;
|
||||
|
||||
const validScreens = ['large', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight'];
|
||||
if (!validScreens.includes(screen)) {
|
||||
return NextResponse.json({ error: 'Invalid screen name' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('Writing files to', path.join(FILE_DIRECTORY(), `${screen}.txt`));
|
||||
const filePath = path.join(FILE_DIRECTORY(), `${screen}.txt`);
|
||||
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const stream: Stream | undefined = await db.get<Stream>(
|
||||
'SELECT * FROM streams_2025_spring_adr WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
console.log('Stream:', stream);
|
||||
|
||||
if (!stream) {
|
||||
return NextResponse.json({ error: 'Stream not found' }, { status: 400 });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, stream.obs_source_name);
|
||||
return NextResponse.json({ message: `${screen} updated successfully.` }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('Error updating active source:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update active source', details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
18
app/api/streams/route.ts
Normal file
18
app/api/streams/route.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../lib/database';
|
||||
import { Stream } from '@/types';
|
||||
import { TABLE_NAMES } from '../../../lib/constants';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
const streams: Stream[] = await db.all(`SELECT * FROM ${TABLE_NAMES.STREAMS}`);
|
||||
return NextResponse.json(streams);
|
||||
} catch (error) {
|
||||
console.error('Error fetching streams:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch streams' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
73
app/api/teams/[teamId]/route.ts
Normal file
73
app/api/teams/[teamId]/route.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '@/lib/database';
|
||||
import { TABLE_NAMES } from '@/lib/constants';
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ teamId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { teamId: teamIdParam } = await params;
|
||||
const teamId = parseInt(teamIdParam);
|
||||
const { team_name } = await request.json();
|
||||
|
||||
if (!team_name) {
|
||||
return NextResponse.json({ error: 'Team name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
const result = await db.run(
|
||||
`UPDATE ${TABLE_NAMES.TEAMS} SET team_name = ? WHERE team_id = ?`,
|
||||
[team_name, teamId]
|
||||
);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return NextResponse.json({ error: 'Team not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Team updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error updating team:', error);
|
||||
return NextResponse.json({ error: 'Failed to update team' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ teamId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { teamId: teamIdParam } = await params;
|
||||
const teamId = parseInt(teamIdParam);
|
||||
const db = await getDatabase();
|
||||
|
||||
await db.run('BEGIN TRANSACTION');
|
||||
|
||||
try {
|
||||
await db.run(
|
||||
`DELETE FROM ${TABLE_NAMES.STREAMS} WHERE team_id = ?`,
|
||||
[teamId]
|
||||
);
|
||||
|
||||
const result = await db.run(
|
||||
`DELETE FROM ${TABLE_NAMES.TEAMS} WHERE team_id = ?`,
|
||||
[teamId]
|
||||
);
|
||||
|
||||
if (result.changes === 0) {
|
||||
await db.run('ROLLBACK');
|
||||
return NextResponse.json({ error: 'Team not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await db.run('COMMIT');
|
||||
return NextResponse.json({ message: 'Team and associated streams deleted successfully' });
|
||||
} catch (error) {
|
||||
await db.run('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting team:', error);
|
||||
return NextResponse.json({ error: 'Failed to delete team' }, { status: 500 });
|
||||
}
|
||||
}
|
37
app/api/teams/route.ts
Normal file
37
app/api/teams/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../lib/database';
|
||||
import { Team } from '@/types';
|
||||
import { TABLE_NAMES } from '@/lib/constants';
|
||||
|
||||
export async function GET() {
|
||||
const db = await getDatabase();
|
||||
const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS}`);
|
||||
return NextResponse.json(teams);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { team_name } = await request.json();
|
||||
|
||||
if (!team_name) {
|
||||
return NextResponse.json({ error: 'Team name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
|
||||
const result = await db.run(
|
||||
`INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`,
|
||||
[team_name]
|
||||
);
|
||||
|
||||
const newTeam: Team = {
|
||||
team_id: result.lastID!,
|
||||
team_name: team_name
|
||||
};
|
||||
|
||||
return NextResponse.json(newTeam, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Error creating team:', error);
|
||||
return NextResponse.json({ error: 'Failed to create team' }, { status: 500 });
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue