Files
OBS_controller/script.js
bryce 0254a30685 init upload
Mostly working
** OBS WS **
- UI isn't connecting
- bot is connecting
2025-07-22 10:50:27 +12:00

706 lines
27 KiB
JavaScript

// --- OBS WebSocket Instance ---
const obs = new OBSWebSocket();
// --- DOM Elements ---
const statusLight = document.getElementById('status-light');
const statusText = document.getElementById('status-text');
const connectDialogButton = document.getElementById('connect-dialog-button');
const connectionDialog = document.getElementById('connection-dialog');
const obsIpInput = document.getElementById('obs-ip');
const obsPortInput = document.getElementById('obs-port');
const obsPasswordInput = document.getElementById('obs-password');
const connectButton = document.getElementById('connect-button');
const closeDialogButton = document.getElementById('close-dialog-button');
const transitionButtons = document.querySelectorAll('.transition-button');
const audioControlsGrid = document.getElementById('audio-controls-grid');
const sceneThumbnailsContainer = document.getElementById('scene-thumbnails');
// New group button references
const toggleAllMicsButton = document.getElementById('toggle-all-mics');
const toggleNonMicsButton = document.getElementById('toggle-non-mics');
// --- Configuration (Editable based on your OBS setup) ---
const MIC_SOURCES = ['Room Mic', 'Headset 1', 'Wireless'];
const MUSIC_SOURCES = ['Music', 'SoundDesk'];
const ALERT_SOURCES = ['Alerts'];
const ALL_AUDIO_SOURCES = [
'Desktop Audio', 'Headset', 'Wireless', 'SoundDesk', 'Kofi Alert', 'Kofi GOAL'
]; // Explicitly list all desired sources
const STINGER_INFO = {
'Stinger 1': { name: 'Stinger 1', duration: 3000 },
'Stinger 2': { name: 'Stinger 2', duration: 3000 }
};
let currentProgramScene = '';
let autoReconnectInterval = null;
const AUTO_RECONNECT_DELAY = 5000;
// --- SVG Icons (same as before) ---
const micIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.2-3c0 3.53-2.64 6.4-6 6.7V22h-2v-4.3c-3.36-.3-6-3.17-6-6.7H3c0 4.3 3.31 7.7 7.5 8.2V22h3v-4.3c4.19-.5 7.5-3.9 7.5-8.2h-2z"/>
</svg>
`;
const micMutedIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19 11h-1.7c0 .72-.11 1.39-.31 2.02l1.46 1.46c.54-.93.85-2 .85-3.48zm-1.88 7.32l-1.41-1.41c-.6.35-1.29.6-2.05.77V22h-2v-3.08c-3.12-.35-5.69-2.9-5.99-6H3c.36 4.67 3.92 8.44 8.5 9v3h3v-3.08c1.37-.24 2.65-.86 3.75-1.78l2.05 2.05L22 19.32 19.12 16.44zM4.2 11h2.08c.17-2.63 2.45-4.8 5.16-4.99V3h2v3.01c.79.08 1.5.29 2.12.57l1.41-1.41c-.99-.61-2.11-1.05-3.3-1.25V1h-2v3.01c-.72.08-1.39.27-2.02.5L4.2 8.79zm7.8-4c-.73 0-1.36.17-1.92.42l4.31 4.31c.14-.59.21-1.2.21-1.83V5c0-1.66-1.34-3-3-3S9 3.34 9 5v2c0 .09.01.17.02.26L11.5 9.74c.16-.01.32-.02.5-.02z"/>
</svg>
`;
const musicIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6zm-2 16c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
</svg>
`;
const musicMutedIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M14.07 1.83L12 3.89V1c-.59.34-1.27.55-2 .55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V4.93l-1.07-1.07zm-2.88 17.58c.28.35.59.66.92.93l1.41 1.41c-.48-.05-.96-.11-1.46-.11-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6zm4-5.32c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM.67 2.05l1.63 1.63C1.65 4.96 1 6.43 1 8c0 2.11 1.16 3.99 2.87 5.06l1.63 1.63C4.19 14.28 3 16.03 3 18c0 3.87 3.13 7 7 7 2.62 0 4.91-1.43 6.13-3.53l2.05 2.05 1.41-1.41L2.08.64.67 2.05zm10.7 10.7L12 13.91V11c-.59-.34-1.27-.55-2-.55-1.57 0-2.92.83-3.64 2.07L7.4 15.11C8.25 14.16 9.09 13.56 10 13.56c.73 0 1.36.17 1.92.42z"/>
</svg>
`;
const alertIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
`;
const alertMutedIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm1-4h-2V7h2v6z"/>
</svg>
`;
// Group Icons (using generic ones for now, customize if needed)
const groupMicsIcon = micIcon; // Use the regular mic icon for the group
const groupMicsMutedIcon = micMutedIcon; // Use the muted mic icon for the group
// For "Other Audio" group, perhaps a speaker or headphones icon?
// Using the music icon for now, you can create a speaker/headphones SVG if you like.
const groupNonMicsIcon = musicIcon;
const groupNonMicsMutedIcon = musicMutedIcon;
// --- Connection Logic (same as before, with .classList.add/remove fix) ---
function updateConnectionStatus(isConnected, reconnecting = false) {
if (isConnected) {
statusLight.className = 'status-light green';
statusText.textContent = 'Connected';
clearInterval(autoReconnectInterval);
autoReconnectInterval = null;
} else {
statusLight.className = reconnecting ? 'status-light amber' : 'status-light red';
statusText.textContent = reconnecting ? 'Attempting to Reconnect...' : 'Disconnected';
if (!reconnecting && !autoReconnectInterval) {
autoReconnectInterval = setInterval(attemptConnect, AUTO_RECONNECT_DELAY);
}
}
}
async function attemptConnect() {
const ip = obsIpInput.value || '127.0.0.1';
const port = obsPortInput.value || '4455';
const password = obsPasswordInput.value || 'dAJyC3vMggSchQPN';
localStorage.setItem('obsIp', ip);
localStorage.setItem('obsPort', port);
localStorage.setItem('obsPassword', password);
updateConnectionStatus(false, true);
try {
console.log('Attempting to connect to OBS WebSocket...');
await obs.connect(`ws://${ip}:${port}`, password);
console.log('Connect method returned. OBS Connection Opened event will fire if successful.');
updateConnectionStatus(true);
connectionDialog.classList.remove('show'); // Use class to hide dialog
} catch (error) {
console.error('Failed to connect to OBS:', error);
updateConnectionStatus(false);
}
}
async function initializeOBSState() {
try {
const { studioModeEnabled } = await obs.call('GetStudioModeEnabled');
if (!studioModeEnabled) {
await obs.call('SetStudioModeEnabled', { studioModeEnabled: true });
console.log('Studio Mode enabled.');
} else {
console.log('Studio Mode already enabled.');
}
} catch (error) {
console.error('Error setting Studio Mode:', error);
}
await updateSceneThumbnails();
try {
const { currentProgramSceneName } = await obs.call('GetCurrentProgramScene');
currentProgramScene = currentProgramSceneName;
highlightActiveThumbnail(currentProgramScene);
} catch (error) {
console.error('Error getting current program scene:', error);
}
await updateAudioControls();
await updateGroupMuteButtons(); // Update the state of group mute buttons
}
// --- Event Listeners for Connection Dialog ---
connectDialogButton.addEventListener('click', () => {
connectionDialog.classList.add('show'); // Use class to show dialog
});
closeDialogButton.addEventListener('click', () => {
connectionDialog.classList.remove('show'); // Use class to hide dialog
});
connectButton.addEventListener('click', attemptConnect);
// Load saved settings on page load
window.addEventListener('load', () => {
obsIpInput.value = localStorage.getItem('obsIp') || '192.168.0.124';
obsPortInput.value = localStorage.getItem('obsPort') || '4455';
obsPasswordInput.value = localStorage.getItem('obsPassword') || 'dAJyC3vMggSchQPN';
attemptConnect();
});
// --- OBS Event Handlers ---
obs.on('ConnectionOpened', async () => {
console.log('OBS Connection Opened Event Fired!');
updateConnectionStatus(true);
// ADD A SMALL DELAY HERE
setTimeout(async () => {
try {
await initializeOBSState(); // Keep this one here
console.log('OBS State fully initialized via ConnectionOpened event after delay.');
} catch (error) {
console.error('Error during delayed OBS state initialization:', error);
}
}, 150); // Try 150ms delay. Adjust if needed (50-500ms)
});
obs.on('ConnectionClosed', () => {
console.log('OBS Connection Closed.');
updateConnectionStatus(false);
});
obs.on('ConnectionError', (error) => {
console.error('OBS Connection Error:', error);
updateConnectionStatus(false);
});
obs.on('CurrentProgramSceneChanged', async (data) => {
console.log('Program Scene Changed to:', data.sceneName);
currentProgramScene = data.sceneName;
highlightActiveThumbnail(currentProgramScene);
// When scene changes, mute states might change. Re-evaluate and update group buttons.
await updateGroupMuteButtons();
});
obs.on('CurrentPreviewSceneChanged', (data) => {
console.log('Preview Scene Changed to:', data.sceneName);
highlightActiveThumbnail(data.sceneName, true);
});
obs.on('SceneListChanged', async () => {
console.log('Scene List Changed. Updating thumbnails...');
await updateSceneThumbnails();
});
obs.on('InputVolumeMeters', (data) => {
data.inputs.forEach(input => {
const slider = document.querySelector(`.volume-slider[data-source="${input.inputName}"]`);
if (slider && !slider.classList.contains('dragging')) {
const volumePercent = Math.round(Math.pow(10, input.inputVolumeDb / 20) * 100);
slider.value = Math.max(0, Math.min(100, volumePercent));
}
});
});
obs.on('InputMuteStateChanged', async (data) => {
updateMuteButtonIcon(data.inputName, data.inputMuted);
// After individual mute state changes, re-evaluate group mute button states
await updateGroupMuteButtons();
});
// --- Scene Swapping Logic (same as before) ---
transitionButtons.forEach(button => {
button.addEventListener('click', async () => {
const transitionType = button.dataset.transition;
const duration = parseInt(button.dataset.duration);
const stingerName = button.dataset.stingerName;
if (!obs.connected) {
alert('Not connected to OBS!');
return;
}
const sceneBeforeTransition = currentProgramScene;
try {
await applyActionsForScene(sceneBeforeTransition, 'leaving');
if (transitionType === 'stinger' && stingerName) {
await obs.call('SetCurrentSceneTransition', { sceneTransitionName: stingerName });
await obs.call('TransitionToProgram');
} else {
await obs.call('SetCurrentSceneTransition', {
sceneTransitionName: transitionType
});
if (duration !== undefined) {
await obs.call('SetCurrentSceneTransitionDuration', {
sceneTransitionDuration: duration
});
}
await obs.call('TransitionToProgram');
}
console.log(`Transitioned with ${transitionType}`);
} catch (error) {
console.error('Error during scene transition:', error);
alert(`Error performing transition: ${error.message}`);
}
});
});
async function applyActionsForScene(sceneName, actionType) {
console.log(`Applying ${actionType} actions for scene: ${sceneName}`);
switch (sceneName) {
case 'Roving':
if (actionType === 'leaving') {
await setInputMuteState('Room Mic', true);
await setInputMuteState('Music', true);
await setInputMuteState('Wireless', false);
await toggleRecording(true);
showAlert('Action: Set stream marker for Start of B-Roll / Interview');
}
break;
case 'Room 1':
case 'Room 2':
if (actionType === 'leaving') {
await setInputMuteState('Wireless', true);
await setInputMuteState('Music', false);
await setInputMuteState('SoundDesk', false);
await toggleRecording(false);
showAlert('Action: Set marker - End of B-Roll / Interview');
}
break;
case 'BRB / Break':
if (actionType === 'leaving') {
await muteAllMicsGroup(true); // Call the group mute function
}
break;
case 'Starting':
if (actionType === 'leaving') {
await muteAllMicsGroup(true); // Call the group mute function
await setInputMuteState('Alerts', false);
await setInputMuteState('Music', false);
}
break;
case 'END / Raiding':
if (actionType === 'leaving') {
await setInputMuteState('Music', true);
await setInputMuteState('SoundDesk', true);
await setInputMuteState('Wireless', true);
await setInputMuteState('Headset 1', false);
}
break;
default:
console.log(`No specific ${actionType} actions for scene: ${sceneName}`);
break;
}
}
// --- Audio Control Logic with new Mute/Unmute Buttons (same as before) ---
async function updateAudioControls() {
if (!obs.connected) return;
audioControlsGrid.innerHTML = '';
for (const sourceName of ALL_AUDIO_SOURCES) {
const audioSourceDiv = document.createElement('div');
audioSourceDiv.classList.add('audio-source');
const label = document.createElement('label');
label.textContent = sourceName;
label.htmlFor = `volume-${sourceName.replace(/\s/g, '-')}`;
const slider = document.createElement('input');
slider.type = 'range';
slider.id = `volume-${sourceName.replace(/\s/g, '-')}`;
slider.classList.add('volume-slider');
slider.setAttribute('orient', 'vertical');
slider.min = '0';
slider.max = '100';
slider.step = '1';
slider.dataset.source = sourceName;
const muteButton = document.createElement('button');
muteButton.classList.add('mute-toggle-button');
muteButton.dataset.source = sourceName;
let iconHtml;
if (MIC_SOURCES.includes(sourceName)) {
iconHtml = micIcon;
} else if (MUSIC_SOURCES.includes(sourceName)) {
iconHtml = musicIcon;
} else if (ALERT_SOURCES.includes(sourceName)) {
iconHtml = alertIcon;
} else {
iconHtml = '';
}
muteButton.innerHTML = iconHtml;
audioSourceDiv.appendChild(label);
audioSourceDiv.appendChild(slider);
audioSourceDiv.appendChild(muteButton);
audioControlsGrid.appendChild(audioSourceDiv);
try {
const { inputVolumeDb } = await obs.call('GetInputVolume', { inputName: sourceName });
const volumePercent = Math.round(Math.pow(10, inputVolumeDb / 20) * 100);
slider.value = Math.max(0, Math.min(100, volumePercent));
} catch (err) {
console.error(`Error getting volume for ${sourceName}:`, err);
}
try {
const { inputMuted } = await obs.call('GetInputMute', { inputName: sourceName });
updateMuteButtonIcon(sourceName, inputMuted);
} catch (err) {
console.error(`Error getting mute state for ${sourceName}:`, err);
}
}
document.querySelectorAll('.volume-slider').forEach(slider => {
slider.addEventListener('mousedown', () => slider.classList.add('dragging'));
slider.addEventListener('mouseup', () => slider.classList.remove('dragging'));
slider.addEventListener('input', async (e) => {
const source = e.target.dataset.source;
const volume = parseFloat(e.target.value);
const volumeDb = 20 * Math.log10(Math.max(0.0001, volume / 100));
try {
await obs.call('SetInputVolume', { inputName: source, inputVolumeDb: volumeDb });
} catch (error) {
console.error(`Error setting volume for ${source}:`, error);
}
});
});
document.querySelectorAll('.mute-toggle-button').forEach(button => {
button.addEventListener('click', async (e) => {
const source = e.currentTarget.dataset.source;
const isMuted = e.currentTarget.classList.contains('muted');
await setInputMuteState(source, !isMuted);
});
});
}
function updateMuteButtonIcon(sourceName, isMuted) {
const button = document.querySelector(`.mute-toggle-button[data-source="${sourceName}"]`);
if (!button) return;
let iconToUse;
if (isMuted) {
if (MIC_SOURCES.includes(sourceName)) {
iconToUse = micMutedIcon;
} else if (MUSIC_SOURCES.includes(sourceName) || ALERT_SOURCES.includes(sourceName)) {
// Use muted music icon for both music and alerts, or make a separate alert muted icon
iconToUse = musicMutedIcon;
} else {
iconToUse = '';
}
button.classList.add('muted');
} else {
if (MIC_SOURCES.includes(sourceName)) {
iconToUse = micIcon;
} else if (MUSIC_SOURCES.includes(sourceName)) {
iconToUse = musicIcon;
} else if (ALERT_SOURCES.includes(sourceName)) {
iconToUse = alertIcon;
} else {
iconToUse = '';
}
button.classList.remove('muted');
}
button.innerHTML = iconToUse;
}
async function setInputMuteState(sourceName, mute) {
if (!obs.connected) {
console.warn(`Not connected to OBS, cannot mute/unmute ${sourceName}.`);
return;
}
try {
await obs.call('SetInputMute', { inputName: sourceName, inputMuted: mute });
console.log(`${sourceName} ${mute ? 'muted' : 'unmuted'}`);
// OBS will fire InputMuteStateChanged, which will then call updateMuteButtonIcon and updateGroupMuteButtons
} catch (error) {
console.error(`Error setting mute state for ${sourceName}:`, error);
}
}
// --- Mute/Unmute Groups - NEW TOGGLE LOGIC ---
// Initial icon setup for group buttons
toggleAllMicsButton.innerHTML = groupMicsIcon + '<span>Mics</span>';
toggleNonMicsButton.innerHTML = groupNonMicsIcon + '<span>Other Audio</span>';
toggleAllMicsButton.addEventListener('click', async (e) => {
const isMuted = e.currentTarget.classList.contains('muted'); // Check if currently showing muted state
await muteAllMicsGroup(!isMuted); // Toggle the state
});
toggleNonMicsButton.addEventListener('click', async (e) => {
const isMuted = e.currentTarget.classList.contains('muted');
await muteNonMicsGroup(!isMuted);
});
async function muteAllMicsGroup(mute) {
// Determine the target state for all mics (either all mute or all unmute)
// Only execute if OBS is connected
if (!obs.connected) {
console.warn('Not connected to OBS, cannot mute/unmute group.');
return;
}
try {
for (const mic of MIC_SOURCES) {
await setInputMuteState(mic, mute);
}
console.log(`All mics group ${mute ? 'muted' : 'unmuted'}.`);
} catch (error) {
console.error('Error muting/unmuting mic group:', error);
}
}
async function muteNonMicsGroup(mute) {
const nonMics = ALL_AUDIO_SOURCES.filter(source => !MIC_SOURCES.includes(source));
if (!obs.connected) {
console.warn('Not connected to OBS, cannot mute/unmute group.');
return;
}
try {
for (const source of nonMics) {
await setInputMuteState(source, mute);
}
console.log(`All non-mic group ${mute ? 'muted' : 'unmuted'}.`);
} catch (error) {
console.error('Error muting/unmuting non-mic group:', error);
}
}
// Function to update the visual state of the group mute buttons
async function updateGroupMuteButtons() {
if (!obs.connected) {
toggleAllMicsButton.classList.remove('muted');
toggleAllMicsButton.innerHTML = groupMicsIcon + '<span>Mics</span>';
toggleNonMicsButton.classList.remove('muted');
toggleNonMicsButton.innerHTML = groupNonMicsIcon + '<span>Other Audio</span>';
return;
}
// Check state of all mic sources
let allMicsMuted = true;
for (const mic of MIC_SOURCES) {
try {
const { inputMuted } = await obs.call('GetInputMute', { inputName: mic });
if (!inputMuted) {
allMicsMuted = false;
break; // If any mic is unmuted, the group is not fully muted
}
} catch (error) {
console.warn(`Could not get mute state for ${mic} (might not exist):`, error.message);
allMicsMuted = false; // Treat non-existent as unmuted for group status
break;
}
}
if (allMicsMuted) {
toggleAllMicsButton.classList.add('muted');
toggleAllMicsButton.innerHTML = groupMicsMutedIcon + '<span>Mics</span>';
} else {
toggleAllMicsButton.classList.remove('muted');
toggleAllMicsButton.innerHTML = groupMicsIcon + '<span>Mics</span>';
}
// Check state of all non-mic sources
let allNonMicsMuted = true;
const nonMics = ALL_AUDIO_SOURCES.filter(source => !MIC_SOURCES.includes(source));
for (const source of nonMics) {
try {
const { inputMuted } = await obs.call('GetInputMute', { inputName: source });
if (!inputMuted) {
allNonMicsMuted = false;
break;
}
} catch (error) {
console.warn(`Could not get mute state for ${source} (might not exist):`, error.message);
allNonMicsMuted = false;
break;
}
}
if (allNonMicsMuted) {
toggleNonMicsButton.classList.add('muted');
toggleNonMicsButton.innerHTML = groupNonMicsMutedIcon + '<span>Other Audio</span>';
} else {
toggleNonMicsButton.classList.remove('muted');
toggleNonMicsButton.innerHTML = groupNonMicsIcon + '<span>Other Audio</span>';
}
}
// --- Recording Control (same as before) ---
async function toggleRecording(start) {
if (!obs.connected) {
console.warn('Not connected to OBS, cannot toggle recording.');
return;
}
try {
const { outputActive } = await obs.call('GetRecordStatus');
if (start && !outputActive) {
await obs.call('StartRecord');
console.log('Recording started.');
} else if (!start && outputActive) {
await obs.call('StopRecord');
console.log('Recording stopped.');
} else {
console.log(`Recording is already ${outputActive ? 'active' : 'inactive'}. No change.`);
}
} catch (error) {
console.error('Error toggling recording:', error);
}
}
// --- Alerts (for Stream Markers) ---
function showAlert(message) {
alert(message);
console.log('Stream Marker Alert:', message);
}
// --- Scene Chooser (Studio Mode) (same as before) ---
async function updateSceneThumbnails() {
if (!obs.connected) return;
try {
const { scenes } = await obs.call('GetSceneList');
sceneThumbnailsContainer.innerHTML = '';
const { currentProgramSceneName } = await obs.call('GetCurrentProgramScene');
const { currentPreviewSceneName } = await obs.call('GetCurrentPreviewScene');
for (const scene of scenes) {
const sceneName = scene.sceneName;
const thumbnailDiv = document.createElement('div');
thumbnailDiv.classList.add('scene-thumbnail');
thumbnailDiv.dataset.sceneName = sceneName;
const img = document.createElement('img');
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
img.alt = sceneName;
img.style.transition = 'opacity 0.5s ease-in-out';
const span = document.createElement('span');
span.textContent = sceneName;
thumbnailDiv.appendChild(img);
thumbnailDiv.appendChild(span);
sceneThumbnailsContainer.appendChild(thumbnailDiv);
if (sceneName === currentPreviewSceneName) {
thumbnailDiv.classList.add('active');
} else if (sceneName === currentProgramSceneName) {
// Keep this empty, highlightActiveThumbnail handles un-previewing
}
thumbnailDiv.addEventListener('click', async () => {
if (!obs.connected) return;
try {
await obs.call('SetCurrentPreviewScene', { sceneName: sceneName });
console.log(`Set preview to: ${sceneName}`);
highlightActiveThumbnail(sceneName, true);
} catch (error) {
console.error(`Error setting preview scene to ${sceneName}:`, error);
}
});
}
startThumbnailRefresh();
} catch (error) {
console.error('Error fetching scenes for thumbnails:', error);
}
}
function highlightActiveThumbnail(sceneName, isPreview = false) {
document.querySelectorAll('.scene-thumbnail').forEach(thumb => {
if (isPreview) {
// If setting a preview, remove active from all others, then add to the target
if (thumb.dataset.sceneName === sceneName) {
thumb.classList.add('active');
} else {
thumb.classList.remove('active');
}
}
// No explicit handling for program scene in UI unless desired for a different visual cue
});
}
let thumbnailRefreshInterval = null;
const THUMBNAIL_REFRESH_RATE = 1000;
async function startThumbnailRefresh() {
if (thumbnailRefreshInterval) {
clearInterval(thumbnailRefreshInterval);
}
thumbnailRefreshInterval = setInterval(async () => {
if (!obs.connected) {
clearInterval(thumbnailRefreshInterval);
thumbnailRefreshInterval = null;
return;
}
try {
const { scenes } = await obs.call('GetSceneList');
console.log('OBS Scenes List:', scenes);
for (const scene of scenes) {
const sceneName = scene.sceneName;
const thumbnailDiv = document.querySelector(`.scene-thumbnail[data-scene-name="${sceneName}"]`);
if (thumbnailDiv) {
const img = thumbnailDiv.querySelector('img');
if (img) {
const { imageData } = await obs.call('GetSceneScreenshot', {
sceneName: sceneName,
imageFormat: 'png',
sourceWidth: 200,
sourceHeight: 112
});
img.style.opacity = '0';
setTimeout(() => {
img.src = imageData;
img.style.opacity = '1';
}, 100);
}
}
}
} catch (error) {
console.error('Error refreshing thumbnails:', error);
}
}, THUMBNAIL_REFRESH_RATE);
}
obs.on('ConnectionOpened', async () => {
console.log('OBS Connection Opened!');
updateConnectionStatus(true);
await initializeOBSState();
});