431 lines
16 KiB
JavaScript
431 lines
16 KiB
JavaScript
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 = `
|
|
<span class="scene-name">${icon} ${scene.name}</span>
|
|
<button class="btn-close btn-close-white remove-scene-btn" aria-label="Remove"></button>
|
|
`;
|
|
sceneListEl.appendChild(item);
|
|
});
|
|
}
|
|
|
|
async function updatePreview(sceneId) {
|
|
const scene = getScene(sceneId);
|
|
if (!scene) return;
|
|
|
|
activePreviewSceneId = scene.id;
|
|
|
|
stopStream(previewPanel);
|
|
previewPanel.innerHTML = '<span class="panel-label">PREVIEW</span><div class="scene-content"></div>';
|
|
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 = '<span class="panel-label">PREVIEW</span><div class="scene-content">Select a Scene</div>';
|
|
previewPanel.style.backgroundColor = '#000';
|
|
previewPanel.style.backgroundImage = 'none';
|
|
renderSceneList();
|
|
}
|
|
|
|
function clearProgram() {
|
|
stopStream(programPanel);
|
|
activeProgramSceneId = null;
|
|
programPanel.innerHTML = '<span class="panel-label">PROGRAM (ON AIR)</span><div class="scene-content"></div>';
|
|
programPanel.style.backgroundColor = '#440000';
|
|
programPanel.style.backgroundImage = 'none';
|
|
}
|
|
|
|
function getIconForSceneType(type) {
|
|
switch (type) {
|
|
case 'color': return '<i class="bi bi-palette-fill"></i>';
|
|
case 'image': return '<i class="bi bi-image-fill"></i>';
|
|
case 'video': return '<i class="bi bi-film"></i>';
|
|
case 'camera': return '<i class="bi bi-camera-video-fill"></i>';
|
|
default: return '<i class="bi bi-question-circle-fill"></i>';
|
|
}
|
|
}
|
|
|
|
// --- 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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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();
|
|
}); |