document.addEventListener('DOMContentLoaded', function () { // UI Elements const sceneListEl = document.querySelector('.scene-list'); const previewPanel = document.getElementById('preview-panel'); const programPanel = document.getElementById('program-panel'); const cutButton = document.getElementById('cut-button'); // Modal Elements const addSceneBtn = document.getElementById('add-scene-btn'); const modal = document.getElementById('add-scene-modal'); const modalCloseBtn = document.getElementById('modal-close-btn'); const modalCancelBtn = document.getElementById('modal-cancel-btn'); const modalSaveBtn = document.getElementById('modal-save-btn'); const sceneNameInput = document.getElementById('scene-name-input'); const sceneTypeSelect = document.getElementById('scene-type-select'); // Source settings elements const colorSettings = document.getElementById('source-color-group'); const imageSettings = document.getElementById('source-image-group'); const videoSettings = document.getElementById('source-video-group'); const cameraSettings = document.getElementById('source-camera-group'); const sceneColorInput = document.getElementById('scene-color-input'); const sceneImageFileInput = document.getElementById('scene-image-file-input'); const sceneVideoFileInput = document.getElementById('scene-video-file-input'); // Camera specific elements const cameraPermissionGroup = document.getElementById('camera-permission-group'); const grantCameraPermissionBtn = document.getElementById('grant-camera-permission-btn'); const cameraDeviceSelection = document.getElementById('camera-device-selection'); const sceneCameraDeviceInput = document.getElementById('scene-camera-device-input'); const cameraErrorMessage = document.getElementById('camera-error-message'); // Data Store let scenes = []; let activePreviewSceneId = null; let activeProgramSceneId = null; let sceneIdCounter = 0; let activeStreams = {}; // To hold references to active MediaStream objects let cameraPermissionGranted = false; // --- STREAM MANAGEMENT --- function stopStream(panel) { const panelId = panel.id; if (activeStreams[panelId]) { activeStreams[panelId].getTracks().forEach(track => track.stop()); delete activeStreams[panelId]; } const videoEl = panel.querySelector('video'); if (videoEl) { videoEl.remove(); } } // --- DATA MANAGEMENT --- function addScene(name, type, value) { const newScene = { id: sceneIdCounter++, name: name, type: type, value: value }; scenes.push(newScene); renderSceneList(); return newScene; } function removeScene(id) { scenes = scenes.filter(scene => scene.id !== id); if (activePreviewSceneId === id) { clearPreview(); } if (activeProgramSceneId === id) { clearProgram(); } renderSceneList(); } function getScene(id) { return scenes.find(scene => scene.id === id); } // --- UI RENDERING --- function renderSceneList() { sceneListEl.innerHTML = ''; // Clear existing list scenes.forEach(scene => { const icon = getIconForSceneType(scene.type); const item = document.createElement('div'); item.className = 'list-group-item list-group-item-action'; item.dataset.sceneId = scene.id; if (scene.id === activePreviewSceneId) { item.classList.add('active'); } item.innerHTML = ` ${icon} ${scene.name} `; sceneListEl.appendChild(item); }); } async function updatePreview(sceneId) { const scene = getScene(sceneId); if (!scene) return; activePreviewSceneId = scene.id; stopStream(previewPanel); previewPanel.innerHTML = 'PREVIEW
'; previewPanel.style.backgroundColor = '#000'; previewPanel.style.backgroundImage = 'none'; previewPanel.querySelector('.scene-content').textContent = scene.name; switch (scene.type) { case 'color': previewPanel.style.backgroundColor = scene.value; break; case 'image': previewPanel.style.backgroundImage = `url('${scene.value}')`; break; case 'video': case 'camera': const video = document.createElement('video'); video.muted = true; video.loop = scene.type === 'video'; video.autoplay = true; video.style.width = '100%'; video.style.height = '100%'; video.style.objectFit = 'cover'; if (scene.type === 'video') { video.src = scene.value; } else { // camera try { const constraints = scene.value ? { video: { deviceId: { exact: scene.value } } } : { video: true }; const stream = await navigator.mediaDevices.getUserMedia(constraints); video.srcObject = stream; activeStreams[previewPanel.id] = stream; } catch (err) { console.error("Error accessing camera:", err); previewPanel.querySelector('.scene-content').textContent = 'Camera Error!'; previewPanel.style.backgroundColor = 'red'; return; } } previewPanel.querySelector('.scene-content').appendChild(video); break; default: previewPanel.style.backgroundColor = '#000'; break; } renderSceneList(); } function clearPreview() { stopStream(previewPanel); activePreviewSceneId = null; previewPanel.innerHTML = 'PREVIEW
Select a Scene
'; previewPanel.style.backgroundColor = '#000'; previewPanel.style.backgroundImage = 'none'; renderSceneList(); } function clearProgram() { stopStream(programPanel); activeProgramSceneId = null; programPanel.innerHTML = 'PROGRAM (ON AIR)
'; programPanel.style.backgroundColor = '#440000'; programPanel.style.backgroundImage = 'none'; } function getIconForSceneType(type) { switch (type) { case 'color': return ''; case 'image': return ''; case 'video': return ''; case 'camera': return ''; default: return ''; } } // --- CAMERA & DEVICE MANAGEMENT --- function resetCameraUI() { cameraPermissionGranted = false; if (sceneCameraDeviceInput) { sceneCameraDeviceInput.innerHTML = ''; // Clear dropdown } if (cameraErrorMessage) { cameraErrorMessage.textContent = ''; // Clear any old errors } if (cameraDeviceSelection) { cameraDeviceSelection.style.display = 'none'; // Hide device selector } if (cameraPermissionGroup) { cameraPermissionGroup.style.display = 'block'; // Show permission button } } function showCameraUI() { if (cameraErrorMessage) { cameraErrorMessage.textContent = ''; } if (cameraPermissionGranted) { if (cameraPermissionGroup) { cameraPermissionGroup.style.display = 'none'; } if (cameraDeviceSelection) { cameraDeviceSelection.style.display = 'block'; } } else { // This is the initial state, handled by resetCameraUI if (cameraPermissionGroup) { cameraPermissionGroup.style.display = 'block'; } if (cameraDeviceSelection) { cameraDeviceSelection.style.display = 'none'; } } } async function requestCameraPermission() { if (!window.isSecureContext) { cameraErrorMessage.textContent = 'Camera access is only available over a secure HTTPS connection.'; return; } let stream = null; try { // First, get a stream. This is necessary to trigger the permission prompt // and to get the device labels. stream = await navigator.mediaDevices.getUserMedia({ video: true }); // Now that we have permission and an active stream, enumerate devices. const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(device => device.kind === 'videoinput'); sceneCameraDeviceInput.innerHTML = ''; // Clear previous options if (videoDevices.length === 0) { cameraErrorMessage.textContent = 'No camera devices were found.'; return; // Exit if no cameras } // Populate the dropdown videoDevices.forEach(device => { const option = document.createElement('option'); option.value = device.deviceId; // Use the device label if available, otherwise a generic name option.textContent = device.label || `Camera ${sceneCameraDeviceInput.length + 1}`; sceneCameraDeviceInput.appendChild(option); }); // We have successfully populated the list, so update the UI cameraPermissionGranted = true; showCameraUI(); } catch (err) { console.error("Could not get camera permissions:", err); let msg = 'An unexpected error occurred.'; if (err.name === 'NotAllowedError') { msg = "Camera permission was denied. You need to grant permission in your browser's site settings to use this feature."; } else if (err.name === 'NotFoundError') { msg = 'No camera was found on your device.'; } else if (err.name === 'NotReadableError') { msg = 'The camera is currently in use by another application.'; } cameraErrorMessage.textContent = msg; } finally { // Stop the stream that was used to get permissions and device labels. if (stream) { stream.getTracks().forEach(track => track.stop()); } } } // --- EVENT LISTENERS --- sceneListEl.addEventListener('click', (e) => { const target = e.target; const sceneItem = target.closest('.list-group-item'); if (!sceneItem) return; const sceneId = parseInt(sceneItem.dataset.sceneId, 10); if (target.classList.contains('remove-scene-btn')) { removeScene(sceneId); } else { updatePreview(sceneId); } }); cutButton.addEventListener('click', () => { if (activePreviewSceneId === null) return; const previewScene = getScene(activePreviewSceneId); activeProgramSceneId = previewScene.id; stopStream(programPanel); programPanel.innerHTML = previewPanel.innerHTML; programPanel.style.backgroundColor = previewPanel.style.backgroundColor; programPanel.style.backgroundImage = previewPanel.style.backgroundImage; if (activeStreams[previewPanel.id]) { activeStreams[programPanel.id] = activeStreams[previewPanel.id]; delete activeStreams[previewPanel.id]; } programPanel.querySelector('.panel-label').textContent = 'PROGRAM (ON AIR)'; const programVideo = programPanel.querySelector('video'); if(programVideo) { programVideo.muted = false; } clearPreview(); }); function showModal() { resetCameraUI(); // Reset camera state every time modal is opened modal.style.display = 'flex'; sceneNameInput.focus(); sceneTypeSelect.dispatchEvent(new Event('change')); } function hideModal() { modal.style.display = 'none'; } addSceneBtn.addEventListener('click', showModal); modalCloseBtn.addEventListener('click', hideModal); modalCancelBtn.addEventListener('click', hideModal); sceneTypeSelect.addEventListener('change', (e) => { const type = e.target.value; colorSettings.style.display = type === 'color' ? 'block' : 'none'; imageSettings.style.display = type === 'image' ? 'block' : 'none'; videoSettings.style.display = type === 'video' ? 'block' : 'none'; cameraSettings.style.display = type === 'camera' ? 'block' : 'none'; if (type === 'camera') { showCameraUI(); } }); grantCameraPermissionBtn.addEventListener('click', requestCameraPermission); async function saveScene() { const name = sceneNameInput.value.trim(); if (!name) { alert('Please enter a scene name.'); return; } const type = sceneTypeSelect.value; let value; let file = null; try { if (type === 'color') { value = sceneColorInput.value; } else if (type === 'camera') { value = sceneCameraDeviceInput.value; if (!value) { alert('Please select a camera device.'); return; } } else { if (type === 'image') { file = sceneImageFileInput.files[0]; if (!file) { alert('Please select an image file.'); return; } } else if (type === 'video') { file = sceneVideoFileInput.files[0]; if (!file) { alert('Please select a video file.'); return; } } modalSaveBtn.disabled = true; modalSaveBtn.innerHTML = ' Uploading...'; const formData = new FormData(); formData.append('file', file); const response = await fetch('upload.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { value = result.filePath; } else { throw new Error(result.error || 'Upload failed'); } } const newScene = addScene(name, type, value); updatePreview(newScene.id); sceneNameInput.value = ''; sceneColorInput.value = '#1e90ff'; sceneImageFileInput.value = ''; sceneVideoFileInput.value = ''; sceneTypeSelect.value = 'color'; colorSettings.style.display = 'block'; imageSettings.style.display = 'none'; videoSettings.style.display = 'none'; cameraSettings.style.display = 'none'; hideModal(); } catch (error) { alert('Error: ' + error.message); } finally { modalSaveBtn.disabled = false; modalSaveBtn.textContent = 'Save Scene'; } } modalSaveBtn.addEventListener('click', saveScene); // --- INITIALIZATION --- function initialize() { addScene('Starting Soon Screen', 'image', 'https://images.pexels.com/photos/1762851/pexels-photo-1762851.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'); addScene('Main Camera', 'camera', ''); addScene('Promo Video', 'video', 'https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4'); addScene('Screen Share', 'color', '#006633'); const firstScene = getScene(0); if (firstScene) { updatePreview(firstScene.id); } } initialize(); });