Fix browser source audio control for individual stream management #11
3 changed files with 98 additions and 19 deletions
|
@ -27,9 +27,14 @@ interface GetSceneListResponse {
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Get teams from database
|
// Get teams and streams from database
|
||||||
const db = await getDatabase();
|
const db = await getDatabase();
|
||||||
const teams = await db.all(`SELECT team_id, team_name, group_name, group_uuid FROM ${TABLE_NAMES.TEAMS} WHERE group_name IS NOT NULL OR group_uuid IS NOT NULL`);
|
const teams = await db.all(`SELECT team_id, team_name, group_name, group_uuid FROM ${TABLE_NAMES.TEAMS} WHERE group_name IS NOT NULL OR group_uuid IS NOT NULL`);
|
||||||
|
const streams = await db.all(`
|
||||||
|
SELECT s.*, t.team_name, t.group_name as team_group_name
|
||||||
|
FROM ${TABLE_NAMES.STREAMS} s
|
||||||
|
LEFT JOIN ${TABLE_NAMES.TEAMS} t ON s.team_id = t.team_id
|
||||||
|
`);
|
||||||
|
|
||||||
// Get scenes (groups) from OBS
|
// Get scenes (groups) from OBS
|
||||||
const obs = await getOBSClient();
|
const obs = await getOBSClient();
|
||||||
|
@ -37,6 +42,11 @@ export async function GET() {
|
||||||
const obsData = response as GetSceneListResponse;
|
const obsData = response as GetSceneListResponse;
|
||||||
const obsScenes = obsData.scenes;
|
const obsScenes = obsData.scenes;
|
||||||
|
|
||||||
|
// Helper function to clean OBS names (matching obsClient.js logic)
|
||||||
|
const cleanObsName = (name: string): string => {
|
||||||
|
return name.toLowerCase().replace(/\s+/g, '_');
|
||||||
|
};
|
||||||
|
|
||||||
// Compare database groups with OBS scenes using both UUID and name
|
// Compare database groups with OBS scenes using both UUID and name
|
||||||
const verification = teams.map(team => {
|
const verification = teams.map(team => {
|
||||||
let exists_in_obs = false;
|
let exists_in_obs = false;
|
||||||
|
@ -75,17 +85,42 @@ export async function GET() {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Generate expected stream scene names based on the database
|
||||||
|
const expectedStreamScenes = streams.map(stream => {
|
||||||
|
if (stream.team_group_name) {
|
||||||
|
const cleanGroupName = cleanObsName(stream.team_group_name);
|
||||||
|
const cleanStreamName = cleanObsName(stream.name);
|
||||||
|
return `${cleanGroupName}_${cleanStreamName}_stream`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
// Check for orphaned scenes - scenes that exist in OBS but aren't in our database
|
||||||
|
const orphanedScenes = obsScenes.filter(scene => {
|
||||||
|
// Check if it's a team scene
|
||||||
|
const isTeamScene = teams.some(team =>
|
||||||
|
team.group_uuid === scene.sceneUuid || team.group_name === scene.sceneName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if it's an expected stream scene
|
||||||
|
const isStreamScene = expectedStreamScenes.includes(scene.sceneName);
|
||||||
|
|
||||||
|
// Check if it's a system scene
|
||||||
|
const isSystemScene = SYSTEM_SCENES.includes(scene.sceneName);
|
||||||
|
|
||||||
|
// It's orphaned if it's none of the above
|
||||||
|
return !isTeamScene && !isStreamScene && !isSystemScene;
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
teams_with_groups: verification,
|
teams_with_groups: verification,
|
||||||
obs_scenes: obsScenes.map(s => ({ name: s.sceneName, uuid: s.sceneUuid })),
|
obs_scenes: obsScenes.map(s => ({ name: s.sceneName, uuid: s.sceneUuid })),
|
||||||
|
expected_stream_scenes: expectedStreamScenes,
|
||||||
missing_in_obs: verification.filter(team => !team.exists_in_obs),
|
missing_in_obs: verification.filter(team => !team.exists_in_obs),
|
||||||
name_mismatches: verification.filter(team => team.name_changed),
|
name_mismatches: verification.filter(team => team.name_changed),
|
||||||
orphaned_in_obs: obsScenes.filter(scene =>
|
orphaned_in_obs: orphanedScenes.map(s => ({ name: s.sceneName, uuid: s.sceneUuid }))
|
||||||
!teams.some(team => team.group_uuid === scene.sceneUuid || team.group_name === scene.sceneName) &&
|
|
||||||
!SYSTEM_SCENES.includes(scene.sceneName)
|
|
||||||
).map(s => ({ name: s.sceneName, uuid: s.sceneUuid }))
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -81,10 +81,10 @@ export default function Footer() {
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
{/* Connection Status */}
|
{/* Connection Status */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="mb-4">
|
||||||
|
<h3 className="font-semibold mb-2">OBS Studio</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<div className={`status-dot ${obsStatus?.connected ? 'connected' : 'disconnected'}`}></div>
|
<div className={`status-dot ${obsStatus?.connected ? 'connected' : 'disconnected'}`}></div>
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">OBS Studio</h3>
|
|
||||||
<p className="text-sm opacity-60">
|
<p className="text-sm opacity-60">
|
||||||
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
|
{obsStatus?.connected ? 'Connected' : 'Disconnected'}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -392,6 +392,8 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
|
||||||
const browserSourceExists = inputs.some(input => input.inputName === sourceName);
|
const browserSourceExists = inputs.some(input => input.inputName === sourceName);
|
||||||
|
|
||||||
if (!browserSourceExists) {
|
if (!browserSourceExists) {
|
||||||
|
// Create browser source with detailed audio control settings
|
||||||
|
console.log(`Creating browser source "${sourceName}" with URL: ${url}`);
|
||||||
await obsClient.call('CreateInput', {
|
await obsClient.call('CreateInput', {
|
||||||
sceneName: streamGroupName, // Create in the nested scene
|
sceneName: streamGroupName, // Create in the nested scene
|
||||||
inputName: sourceName,
|
inputName: sourceName,
|
||||||
|
@ -400,20 +402,46 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
url,
|
url,
|
||||||
control_audio: true,
|
control_audio: true, // Enable audio control so OBS can mute the browser source
|
||||||
|
restart_when_active: false,
|
||||||
|
shutdown: false,
|
||||||
|
// Try additional audio-related settings
|
||||||
|
reroute_audio: true,
|
||||||
|
audio_monitoring_type: 0 // Monitor Off
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(`Created browser source "${sourceName}" in nested scene`);
|
console.log(`Created browser source "${sourceName}" in nested scene with URL: ${url}`);
|
||||||
|
|
||||||
// Mute the audio stream for the browser source
|
// Apply additional settings after creation to ensure they stick
|
||||||
|
try {
|
||||||
|
await obsClient.call('SetInputSettings', {
|
||||||
|
inputName: sourceName,
|
||||||
|
inputSettings: {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
url, // Make sure URL is preserved
|
||||||
|
control_audio: true,
|
||||||
|
reroute_audio: true,
|
||||||
|
restart_when_active: false,
|
||||||
|
shutdown: false,
|
||||||
|
audio_monitoring_type: 0
|
||||||
|
},
|
||||||
|
overlay: false // Don't overlay, replace all settings
|
||||||
|
});
|
||||||
|
console.log(`Applied complete settings to "${sourceName}" with URL: ${url}`);
|
||||||
|
} catch (settingsError) {
|
||||||
|
console.error(`Failed to apply settings to "${sourceName}":`, settingsError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mute the browser source audio by default (can be unmuted individually in OBS)
|
||||||
try {
|
try {
|
||||||
await obsClient.call('SetInputMute', {
|
await obsClient.call('SetInputMute', {
|
||||||
inputName: sourceName,
|
inputName: sourceName,
|
||||||
inputMuted: true
|
inputMuted: true
|
||||||
});
|
});
|
||||||
console.log(`Muted audio for browser source "${sourceName}"`);
|
console.log(`Muted browser source audio for "${sourceName}" (can be unmuted in OBS for individual control)`);
|
||||||
} catch (muteError) {
|
} catch (muteError) {
|
||||||
console.error(`Failed to mute audio for "${sourceName}":`, muteError.message);
|
console.error(`Failed to mute browser source audio for "${sourceName}":`, muteError.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add existing source to nested scene
|
// Add existing source to nested scene
|
||||||
|
@ -421,16 +449,32 @@ async function createStreamGroup(groupName, streamName, teamName, url) {
|
||||||
sceneName: streamGroupName,
|
sceneName: streamGroupName,
|
||||||
sourceName: sourceName
|
sourceName: sourceName
|
||||||
});
|
});
|
||||||
|
console.log(`Added existing browser source "${sourceName}" to nested scene`);
|
||||||
|
|
||||||
// Ensure audio is muted for existing source too
|
// Ensure existing browser source has audio control enabled and correct URL
|
||||||
try {
|
try {
|
||||||
|
await obsClient.call('SetInputSettings', {
|
||||||
|
inputName: sourceName,
|
||||||
|
inputSettings: {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
url, // Update URL in case it changed
|
||||||
|
control_audio: true,
|
||||||
|
reroute_audio: true,
|
||||||
|
restart_when_active: false,
|
||||||
|
shutdown: false,
|
||||||
|
audio_monitoring_type: 0
|
||||||
|
},
|
||||||
|
overlay: false // Replace settings, don't overlay
|
||||||
|
});
|
||||||
|
|
||||||
await obsClient.call('SetInputMute', {
|
await obsClient.call('SetInputMute', {
|
||||||
inputName: sourceName,
|
inputName: sourceName,
|
||||||
inputMuted: true
|
inputMuted: true
|
||||||
});
|
});
|
||||||
console.log(`Ensured audio is muted for existing browser source "${sourceName}"`);
|
console.log(`Updated existing browser source "${sourceName}" with URL: ${url} and enabled audio control`);
|
||||||
} catch (muteError) {
|
} catch (settingsError) {
|
||||||
console.error(`Failed to mute audio for existing source "${sourceName}":`, muteError.message);
|
console.error(`Failed to update settings for existing source "${sourceName}":`, settingsError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -614,7 +658,7 @@ async function deleteStreamComponents(streamName, teamName, groupName) {
|
||||||
console.log(`Nested scene "${streamGroupName}" not found:`, error.message);
|
console.log(`Nested scene "${streamGroupName}" not found:`, error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Remove the browser source (if it's not used elsewhere)
|
// 3. Remove the browser source
|
||||||
try {
|
try {
|
||||||
const { inputs } = await obsClient.call('GetInputList');
|
const { inputs } = await obsClient.call('GetInputList');
|
||||||
const browserSource = inputs.find(input => input.inputName === sourceName);
|
const browserSource = inputs.find(input => input.inputName === sourceName);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue