diff --git a/.htaccess b/.htaccess index e2bbc23..d42522a 100644 --- a/.htaccess +++ b/.htaccess @@ -1,18 +1,2 @@ -DirectoryIndex index.php index.html -Options -Indexes -Options -MultiViews - -RewriteEngine On - -# 0) Serve existing files/directories as-is -RewriteCond %{REQUEST_FILENAME} -f [OR] -RewriteCond %{REQUEST_FILENAME} -d -RewriteRule ^ - [L] - -# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists) -RewriteCond %{REQUEST_FILENAME}.php -f -RewriteRule ^(.+?)/?$ $1.php [L] - -# 2) Optional: strip trailing slash for non-directories (keeps .php links working) -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule ^(.+)/$ $1 [R=301,L] +php_value upload_max_filesize 64M +php_value post_max_size 64M \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 93796b1..cdbd218 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -3,8 +3,6 @@ document.addEventListener('DOMContentLoaded', function () { const sceneListEl = document.querySelector('.scene-list'); const previewPanel = document.getElementById('preview-panel'); const programPanel = document.getElementById('program-panel'); - const previewContent = previewPanel.querySelector('.scene-content'); - const programContent = programPanel.querySelector('.scene-content'); const cutButton = document.getElementById('cut-button'); // Modal Elements @@ -19,14 +17,37 @@ document.addEventListener('DOMContentLoaded', function () { // 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 sceneImageUrlInput = document.getElementById('scene-image-url-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) { @@ -46,6 +67,9 @@ document.addEventListener('DOMContentLoaded', function () { if (activePreviewSceneId === id) { clearPreview(); } + if (activeProgramSceneId === id) { + clearProgram(); + } renderSceneList(); } @@ -72,16 +96,17 @@ document.addEventListener('DOMContentLoaded', function () { }); } - function updatePreview(sceneId) { + async function updatePreview(sceneId) { const scene = getScene(sceneId); if (!scene) return; activePreviewSceneId = scene.id; - // Clear previous styles + stopStream(previewPanel); + previewPanel.innerHTML = 'PREVIEW
'; previewPanel.style.backgroundColor = '#000'; previewPanel.style.backgroundImage = 'none'; - previewContent.textContent = scene.name; + previewPanel.querySelector('.scene-content').textContent = scene.name; switch (scene.type) { case 'color': @@ -90,39 +115,132 @@ document.addEventListener('DOMContentLoaded', function () { 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(); // Re-render to show active state + renderSceneList(); } function clearPreview() { + stopStream(previewPanel); activePreviewSceneId = null; - previewContent.textContent = 'Select a Scene'; + 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 --- - // Scene list interactions (select, remove) sceneListEl.addEventListener('click', (e) => { const target = e.target; const sceneItem = target.closest('.list-group-item'); if (!sceneItem) return; - const sceneId = parseInt(sceneItem.dataset.sceneId); + const sceneId = parseInt(sceneItem.dataset.sceneId, 10); if (target.classList.contains('remove-scene-btn')) { removeScene(sceneId); @@ -131,22 +249,41 @@ document.addEventListener('DOMContentLoaded', function () { } }); - // Transition button cutButton.addEventListener('click', () => { if (activePreviewSceneId === null) return; const previewScene = getScene(activePreviewSceneId); activeProgramSceneId = previewScene.id; - programContent.textContent = previewScene.name; + 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(); }); - // Modal interactions - function showModal() { modal.style.display = 'flex'; sceneNameInput.focus(); } + 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); @@ -157,9 +294,24 @@ document.addEventListener('DOMContentLoaded', function () { 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'; + } + } }); - modalSaveBtn.addEventListener('click', () => { + grantCameraPermissionBtn.addEventListener('click', populateCameraDevices); + + async function saveScene() { const name = sceneNameInput.value.trim(); if (!name) { alert('Please enter a scene name.'); @@ -167,49 +319,83 @@ document.addEventListener('DOMContentLoaded', function () { } const type = sceneTypeSelect.value; let value; + let file = null; - if (type === 'color') { - value = sceneColorInput.value; - } else if (type === 'image') { - value = sceneImageUrlInput.value.trim(); - if (!value) { - alert('Please enter an image URL.'); - return; + 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'; } + } - const newScene = addScene(name, type, value); - updatePreview(newScene.id); - - // Reset form and hide modal - sceneNameInput.value = ''; - sceneColorInput.value = '#1e90ff'; - sceneImageUrlInput.value = ''; - sceneTypeSelect.value = 'color'; - colorSettings.style.display = 'block'; - imageSettings.style.display = 'none'; - hideModal(); - }); + modalSaveBtn.addEventListener('click', saveScene); // --- INITIALIZATION --- function initialize() { - // Add some default scenes - addScene('Main Camera', 'color', '#003366'); + // 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'); - // Set initial program scene for display - const firstScene = scenes[0]; + const firstScene = getScene(0); if (firstScene) { - activeProgramSceneId = firstScene.id; - programContent.textContent = firstScene.name; - programPanel.style.backgroundColor = firstScene.value; - programPanel.style.backgroundImage = 'none'; -} - - // Select the second scene for preview initially - if (scenes.length > 1) { - updatePreview(scenes[1].id); + updatePreview(firstScene.id); } } diff --git a/assets/uploads/images/img_690e7702877914.36415344.png b/assets/uploads/images/img_690e7702877914.36415344.png new file mode 100644 index 0000000..1e8414f Binary files /dev/null and b/assets/uploads/images/img_690e7702877914.36415344.png differ diff --git a/assets/uploads/videos/vid_690e77a9244553.28824953.mp4 b/assets/uploads/videos/vid_690e77a9244553.28824953.mp4 new file mode 100644 index 0000000..aeb8324 Binary files /dev/null and b/assets/uploads/videos/vid_690e77a9244553.28824953.mp4 differ diff --git a/index.php b/index.php index bb41f40..cc48181 100644 --- a/index.php +++ b/index.php @@ -95,7 +95,8 @@ @@ -105,10 +106,25 @@ + + + + -