This commit is contained in:
Flatlogic Bot 2025-11-07 23:05:18 +00:00
parent 1496e8cc1d
commit eb2b31997e

View File

@ -28,6 +28,7 @@ document.addEventListener('DOMContentLoaded', function () {
const grantCameraPermissionBtn = document.getElementById('grant-camera-permission-btn'); const grantCameraPermissionBtn = document.getElementById('grant-camera-permission-btn');
const cameraDeviceSelection = document.getElementById('camera-device-selection'); const cameraDeviceSelection = document.getElementById('camera-device-selection');
const sceneCameraDeviceInput = document.getElementById('scene-camera-device-input'); const sceneCameraDeviceInput = document.getElementById('scene-camera-device-input');
const cameraErrorMessage = document.getElementById('camera-error-message');
// Data Store // Data Store
let scenes = []; let scenes = [];
@ -35,6 +36,7 @@ document.addEventListener('DOMContentLoaded', function () {
let activeProgramSceneId = null; let activeProgramSceneId = null;
let sceneIdCounter = 0; let sceneIdCounter = 0;
let activeStreams = {}; // To hold references to active MediaStream objects let activeStreams = {}; // To hold references to active MediaStream objects
let cameraPermissionGranted = false;
// --- STREAM MANAGEMENT --- // --- STREAM MANAGEMENT ---
function stopStream(panel) { function stopStream(panel) {
@ -129,8 +131,6 @@ document.addEventListener('DOMContentLoaded', function () {
video.src = scene.value; video.src = scene.value;
} else { // camera } else { // camera
try { 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 const constraints = scene.value
? { video: { deviceId: { exact: scene.value } } } ? { video: { deviceId: { exact: scene.value } } }
: { video: true }; : { video: true };
@ -181,55 +181,68 @@ document.addEventListener('DOMContentLoaded', function () {
} }
// --- CAMERA & DEVICE MANAGEMENT --- // --- CAMERA & DEVICE MANAGEMENT ---
async function populateCameraDevices() { function showCameraUI() {
// First, check for a secure context (HTTPS), which is required for getUserMedia. 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) { if (!window.isSecureContext) {
console.error("Camera access requires a secure context (HTTPS)."); cameraErrorMessage.textContent = 'Camera access is only available over a secure HTTPS connection.';
cameraPermissionPrompt.innerHTML = '<p class="text-danger">Camera access is only available over a secure HTTPS connection. Please ensure you are using an https:// URL.</p>';
return; return;
} }
let stream = null;
try { try {
// Request permission first // First, get a stream. This is necessary to trigger the permission prompt
const stream = await navigator.mediaDevices.getUserMedia({ video: true }); // 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 devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput'); const videoDevices = devices.filter(device => device.kind === 'videoinput');
sceneCameraDeviceInput.innerHTML = ''; sceneCameraDeviceInput.innerHTML = ''; // Clear previous options
if (videoDevices.length === 0) { if (videoDevices.length === 0) {
// This case is unlikely if getUserMedia succeeded, but good to have. cameraErrorMessage.textContent = 'No camera devices were found.';
cameraPermissionPrompt.innerHTML = '<p class="text-warning">No camera devices were found.</p>'; return; // Exit if no cameras
return;
} }
// Populate the dropdown
videoDevices.forEach(device => { videoDevices.forEach(device => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = device.deviceId; option.value = device.deviceId;
// Use the device label if available, otherwise a generic name
option.textContent = device.label || `Camera ${sceneCameraDeviceInput.length + 1}`; option.textContent = device.label || `Camera ${sceneCameraDeviceInput.length + 1}`;
sceneCameraDeviceInput.appendChild(option); sceneCameraDeviceInput.appendChild(option);
}); });
// Switch UI // We have successfully populated the list, so update the UI
cameraPermissionPrompt.style.display = 'none'; cameraPermissionGranted = true;
cameraDeviceSelection.style.display = 'block'; showCameraUI();
} catch (err) { } catch (err) {
console.error("Could not get camera permissions or list devices:", err); console.error("Could not get camera permissions:", err);
let errorMessage = 'An unexpected error occurred while trying to access the camera.'; let msg = 'An unexpected error occurred.';
if (err.name === 'NotAllowedError') { 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') { } 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') { } 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 = `<p class="text-danger">${errorMessage}</p>`;
} }
} }
@ -257,14 +270,11 @@ document.addEventListener('DOMContentLoaded', function () {
stopStream(programPanel); stopStream(programPanel);
// Clone the content and style
programPanel.innerHTML = previewPanel.innerHTML; programPanel.innerHTML = previewPanel.innerHTML;
programPanel.style.backgroundColor = previewPanel.style.backgroundColor; programPanel.style.backgroundColor = previewPanel.style.backgroundColor;
programPanel.style.backgroundImage = previewPanel.style.backgroundImage; programPanel.style.backgroundImage = previewPanel.style.backgroundImage;
const previewVideo = previewPanel.querySelector('video'); if (activeStreams[previewPanel.id]) {
if (previewVideo && previewVideo.srcObject) { // It's a stream
// Move the stream from preview to program
activeStreams[programPanel.id] = activeStreams[previewPanel.id]; activeStreams[programPanel.id] = activeStreams[previewPanel.id];
delete activeStreams[previewPanel.id]; delete activeStreams[previewPanel.id];
} }
@ -272,7 +282,7 @@ document.addEventListener('DOMContentLoaded', function () {
programPanel.querySelector('.panel-label').textContent = 'PROGRAM (ON AIR)'; programPanel.querySelector('.panel-label').textContent = 'PROGRAM (ON AIR)';
const programVideo = programPanel.querySelector('video'); const programVideo = programPanel.querySelector('video');
if(programVideo) { if(programVideo) {
programVideo.muted = false; // Unmute in program programVideo.muted = false;
} }
clearPreview(); clearPreview();
@ -281,7 +291,6 @@ document.addEventListener('DOMContentLoaded', function () {
function showModal() { function showModal() {
modal.style.display = 'flex'; modal.style.display = 'flex';
sceneNameInput.focus(); sceneNameInput.focus();
// Reset and update visibility on open
sceneTypeSelect.dispatchEvent(new Event('change')); sceneTypeSelect.dispatchEvent(new Event('change'));
} }
function hideModal() { modal.style.display = 'none'; } function hideModal() { modal.style.display = 'none'; }
@ -298,18 +307,11 @@ document.addEventListener('DOMContentLoaded', function () {
cameraSettings.style.display = type === 'camera' ? 'block' : 'none'; cameraSettings.style.display = type === 'camera' ? 'block' : 'none';
if (type === 'camera') { if (type === 'camera') {
// Reset to initial state showCameraUI();
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); grantCameraPermissionBtn.addEventListener('click', requestCameraPermission);
async function saveScene() { async function saveScene() {
const name = sceneNameInput.value.trim(); const name = sceneNameInput.value.trim();
@ -362,13 +364,12 @@ document.addEventListener('DOMContentLoaded', function () {
const newScene = addScene(name, type, value); const newScene = addScene(name, type, value);
updatePreview(newScene.id); updatePreview(newScene.id);
// Reset form and hide modal
sceneNameInput.value = ''; sceneNameInput.value = '';
sceneColorInput.value = '#1e90ff'; sceneColorInput.value = '#1e90ff';
sceneImageFileInput.value = ''; sceneImageFileInput.value = '';
sceneVideoFileInput.value = ''; sceneVideoFileInput.value = '';
// Don't clear camera list, just reset selection
sceneTypeSelect.value = 'color'; sceneTypeSelect.value = 'color';
colorSettings.style.display = 'block'; colorSettings.style.display = 'block';
imageSettings.style.display = 'none'; imageSettings.style.display = 'none';
videoSettings.style.display = 'none'; videoSettings.style.display = 'none';
@ -387,9 +388,8 @@ document.addEventListener('DOMContentLoaded', function () {
// --- INITIALIZATION --- // --- INITIALIZATION ---
function initialize() { 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('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('Promo Video', 'video', 'https://assets.mixkit.co/videos/preview/mixkit-spinning-around-the-earth-29351-large.mp4');
addScene('Screen Share', 'color', '#006633'); addScene('Screen Share', 'color', '#006633');