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 cameraPermissionPrompt = document.getElementById('camera-permission-prompt'); const grantCameraPermissionBtn = document.getElementById('grant-camera-permission-btn'); const cameraDeviceSelection = document.getElementById('camera-device-selection'); const sceneCameraDeviceInput = document.getElementById('scene-camera-device-input'); // Data Store let scenes = []; let activePreviewSceneId = null; let activeProgramSceneId = null; let sceneIdCounter = 0; let activeStreams = {}; // To hold references to active MediaStream objects // --- 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 { // A deviceId of '' can happen if the initial scene is a camera // before permission is granted. We should ask for default camera. 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 = 'PREVIEWCamera access is only available over a secure HTTPS connection. Please ensure you are using an https:// URL.
'; return; } try { // Request permission first const stream = await navigator.mediaDevices.getUserMedia({ video: true }); // Stop the tracks immediately, we only needed it for permission stream.getTracks().forEach(track => track.stop()); const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(device => device.kind === 'videoinput'); sceneCameraDeviceInput.innerHTML = ''; if (videoDevices.length === 0) { // This case is unlikely if getUserMedia succeeded, but good to have. cameraPermissionPrompt.innerHTML = 'No camera devices were found.
'; return; } videoDevices.forEach(device => { const option = document.createElement('option'); option.value = device.deviceId; option.textContent = device.label || `Camera ${sceneCameraDeviceInput.length + 1}`; sceneCameraDeviceInput.appendChild(option); }); // Switch UI cameraPermissionPrompt.style.display = 'none'; cameraDeviceSelection.style.display = 'block'; } catch (err) { console.error("Could not get camera permissions or list devices:", err); let errorMessage = 'An unexpected error occurred while trying to access the camera.'; if (err.name === 'NotAllowedError') { errorMessage = 'Camera permission was denied. You need to grant permission in your browser\'s site settings to use this feature.'; } else if (err.name === 'NotFoundError') { errorMessage = 'No camera was found on your device. Please connect a camera and try again.'; } else if (err.name === 'NotReadableError') { errorMessage = 'The camera is currently in use by another application or a hardware error occurred.'; } cameraPermissionPrompt.innerHTML = `${errorMessage}
`; } } // --- 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); // Clone the content and style programPanel.innerHTML = previewPanel.innerHTML; programPanel.style.backgroundColor = previewPanel.style.backgroundColor; programPanel.style.backgroundImage = previewPanel.style.backgroundImage; const previewVideo = previewPanel.querySelector('video'); if (previewVideo && previewVideo.srcObject) { // It's a stream // Move the stream from preview to program 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; // Unmute in program } clearPreview(); }); function showModal() { modal.style.display = 'flex'; sceneNameInput.focus(); // Reset and update visibility on open 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') { // Reset to initial state cameraPermissionPrompt.style.display = 'block'; cameraDeviceSelection.style.display = 'none'; // Check if devices are already populated if (sceneCameraDeviceInput.options.length > 0) { cameraPermissionPrompt.style.display = 'none'; cameraDeviceSelection.style.display = 'block'; } } }); grantCameraPermissionBtn.addEventListener('click', populateCameraDevices); 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); // Reset form and hide modal sceneNameInput.value = ''; sceneColorInput.value = '#1e90ff'; sceneImageFileInput.value = ''; sceneVideoFileInput.value = ''; // Don't clear camera list, just reset selection 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() { // Set a default scene that doesn't require permissions on load 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', ''); // Default camera, will ask for permission on use 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(); });