diff --git a/assets/js/main.js b/assets/js/main.js index cdbd218..ff648ab 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -28,6 +28,7 @@ document.addEventListener('DOMContentLoaded', function () { 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 = []; @@ -35,6 +36,7 @@ document.addEventListener('DOMContentLoaded', function () { let activeProgramSceneId = null; let sceneIdCounter = 0; let activeStreams = {}; // To hold references to active MediaStream objects + let cameraPermissionGranted = false; // --- STREAM MANAGEMENT --- function stopStream(panel) { @@ -129,8 +131,6 @@ document.addEventListener('DOMContentLoaded', function () { 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 }; @@ -181,55 +181,68 @@ document.addEventListener('DOMContentLoaded', function () { } // --- CAMERA & DEVICE MANAGEMENT --- - async function populateCameraDevices() { - // First, check for a secure context (HTTPS), which is required for getUserMedia. + function showCameraUI() { + cameraErrorMessage.textContent = ''; + if (cameraPermissionGranted) { + cameraPermissionPrompt.style.display = 'none'; + cameraDeviceSelection.style.display = 'block'; + } else { + cameraPermissionPrompt.style.display = 'block'; + cameraDeviceSelection.style.display = 'none'; + } + } + + async function requestCameraPermission() { if (!window.isSecureContext) { - console.error("Camera access requires a secure context (HTTPS)."); - cameraPermissionPrompt.innerHTML = '

Camera access is only available over a secure HTTPS connection. Please ensure you are using an https:// URL.

'; + cameraErrorMessage.textContent = 'Camera access is only available over a secure HTTPS connection.'; return; } + let stream = null; try { - // Request permission first - const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + // 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 }); - // Stop the tracks immediately, we only needed it for permission - stream.getTracks().forEach(track => track.stop()); - + // 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 = ''; + sceneCameraDeviceInput.innerHTML = ''; // Clear previous options if (videoDevices.length === 0) { - // This case is unlikely if getUserMedia succeeded, but good to have. - cameraPermissionPrompt.innerHTML = '

No camera devices were found.

'; - return; + 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); }); - // Switch UI - cameraPermissionPrompt.style.display = 'none'; - cameraDeviceSelection.style.display = 'block'; + // We have successfully populated the list, so update the UI + cameraPermissionGranted = true; + showCameraUI(); } 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.'; - + console.error("Could not get camera permissions:", err); + let msg = 'An unexpected error occurred.'; if (err.name === 'NotAllowedError') { - errorMessage = 'Camera permission was denied. You need to grant permission in your browser\'s site settings to use this feature.'; + 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') { - errorMessage = 'No camera was found on your device. Please connect a camera and try again.'; + msg = 'No camera was found on your device.'; } else if (err.name === 'NotReadableError') { - errorMessage = 'The camera is currently in use by another application or a hardware error occurred.'; + 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()); } - - cameraPermissionPrompt.innerHTML = `

${errorMessage}

`; } } @@ -257,14 +270,11 @@ document.addEventListener('DOMContentLoaded', function () { 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 + if (activeStreams[previewPanel.id]) { activeStreams[programPanel.id] = activeStreams[previewPanel.id]; delete activeStreams[previewPanel.id]; } @@ -272,7 +282,7 @@ document.addEventListener('DOMContentLoaded', function () { programPanel.querySelector('.panel-label').textContent = 'PROGRAM (ON AIR)'; const programVideo = programPanel.querySelector('video'); if(programVideo) { - programVideo.muted = false; // Unmute in program + programVideo.muted = false; } clearPreview(); @@ -281,7 +291,6 @@ document.addEventListener('DOMContentLoaded', function () { 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'; } @@ -298,18 +307,11 @@ document.addEventListener('DOMContentLoaded', function () { 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'; - } + showCameraUI(); } }); - grantCameraPermissionBtn.addEventListener('click', populateCameraDevices); + grantCameraPermissionBtn.addEventListener('click', requestCameraPermission); async function saveScene() { const name = sceneNameInput.value.trim(); @@ -362,13 +364,12 @@ document.addEventListener('DOMContentLoaded', function () { 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'; @@ -387,9 +388,8 @@ document.addEventListener('DOMContentLoaded', function () { // --- 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('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');