Add Aurora Livecam video player enhancements

- Add SettingsManager class for admin settings (settings.json)
- Add timelapse controls with slider, speed (1x/10x/100x), and reverse playback
- Add daily video player to play videos in main player window
- Add admin settings panel for viewer display and video mode configuration
- Add CSS styles for new player controls
- Include integration guide for existing index.php
This commit is contained in:
Claude
2026-01-10 10:08:17 +00:00
parent cac3768885
commit 367aa4c67b
7 changed files with 1066 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
/**
* Admin Settings Manager - AJAX ohne Reload
*/
const AdminSettings = {
settings: {},
init: function() {
this.loadSettings();
this.setupEventListeners();
},
loadSettings: function() {
fetch(window.location.href, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'settings_action=get'
})
.then(r => r.json())
.then(data => {
if (data.success) {
this.settings = data.settings;
this.updateUI();
}
})
.catch(err => console.error('Settings load error:', err));
},
updateSetting: function(key, value) {
fetch(window.location.href, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `settings_action=update&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`
})
.then(r => r.json())
.then(data => {
if (data.success) {
this.showNotification('✓ Einstellung gespeichert', 'success');
// Sofort UI aktualisieren
this.applySettingImmediately(key, value);
} else {
this.showNotification('✗ Fehler beim Speichern', 'error');
}
})
.catch(err => {
console.error('Settings update error:', err);
this.showNotification('✗ Netzwerkfehler', 'error');
});
},
applySettingImmediately: function(key, value) {
// Sofortige Anwendung ohne Reload
switch(key) {
case 'viewer_display.enabled':
const viewerEl = document.querySelector('.viewer-stat');
if (viewerEl) {
viewerEl.style.display = value === true || value === 'true' ? 'inline-flex' : 'none';
}
break;
case 'viewer_display.min_viewers':
// Wird beim nächsten Heartbeat angewendet
window.minViewersToShow = parseInt(value);
break;
}
},
updateUI: function() {
// Checkbox für Zuschauer-Anzeige
const viewerEnabled = document.getElementById('setting-viewer-enabled');
if (viewerEnabled) {
viewerEnabled.checked = this.settings.viewer_display?.enabled ?? true;
}
// Mindestanzahl
const minViewers = document.getElementById('setting-min-viewers');
if (minViewers) {
minViewers.value = this.settings.viewer_display?.min_viewers ?? 1;
}
// Video-Modus
const playInPlayer = document.getElementById('setting-play-in-player');
if (playInPlayer) {
playInPlayer.checked = this.settings.video_mode?.play_in_player ?? true;
}
const allowDownload = document.getElementById('setting-allow-download');
if (allowDownload) {
allowDownload.checked = this.settings.video_mode?.allow_download ?? true;
}
},
setupEventListeners: function() {
// Zuschauer-Anzeige Toggle
document.getElementById('setting-viewer-enabled')?.addEventListener('change', (e) => {
this.updateSetting('viewer_display.enabled', e.target.checked);
});
// Mindestanzahl Zuschauer
document.getElementById('setting-min-viewers')?.addEventListener('change', (e) => {
this.updateSetting('viewer_display.min_viewers', e.target.value);
});
// Video im Player abspielen
document.getElementById('setting-play-in-player')?.addEventListener('change', (e) => {
this.updateSetting('video_mode.play_in_player', e.target.checked);
});
// Download erlauben
document.getElementById('setting-allow-download')?.addEventListener('change', (e) => {
this.updateSetting('video_mode.allow_download', e.target.checked);
});
},
showNotification: function(message, type) {
const notification = document.createElement('div');
notification.className = `admin-notification ${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 8px;
background: ${type === 'success' ? '#4CAF50' : '#f44336'};
color: white;
font-weight: bold;
z-index: 10000;
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
};
// Initialisierung nur im Admin-Bereich
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('admin-settings-panel')) {
AdminSettings.init();
}
});
+167
View File
@@ -0,0 +1,167 @@
/**
* Timelapse Controller mit Slider, Geschwindigkeit und Rückwärts
*/
const TimelapseController = {
imageFiles: [],
currentIndex: 0,
isPlaying: false,
isReverse: false,
speed: 1,
availableSpeeds: [1, 10, 100],
intervalId: null,
baseInterval: 200, // ms bei 1x
init: function(imageFilesArray) {
this.imageFiles = imageFilesArray;
this.setupControls();
this.updateSlider();
},
setupControls: function() {
const container = document.getElementById('timelapse-controls');
if (!container) return;
container.innerHTML = `
<div class="timelapse-control-bar">
<button id="tl-play-pause" class="tl-btn" title="Play/Pause">
<i class="fas fa-play"></i>
</button>
<button id="tl-reverse" class="tl-btn" title="Rückwärts">
<i class="fas fa-backward"></i>
</button>
<div class="tl-slider-container">
<input type="range" id="tl-slider" min="0" max="100" value="0">
<span id="tl-time-display">00:00:00</span>
</div>
<div class="tl-speed-container">
<button id="tl-speed" class="tl-btn tl-speed-btn">1x</button>
</div>
<button id="tl-back-live" class="tl-btn tl-back-btn" title="Zurück zu Live">
<i class="fas fa-video"></i> Live
</button>
</div>
`;
// Event Listeners
document.getElementById('tl-play-pause').onclick = () => this.togglePlay();
document.getElementById('tl-reverse').onclick = () => this.toggleReverse();
document.getElementById('tl-speed').onclick = () => this.cycleSpeed();
document.getElementById('tl-back-live').onclick = () => this.backToLive();
const slider = document.getElementById('tl-slider');
slider.max = this.imageFiles.length - 1;
slider.oninput = (e) => this.seekTo(parseInt(e.target.value));
},
togglePlay: function() {
this.isPlaying = !this.isPlaying;
const btn = document.getElementById('tl-play-pause');
btn.innerHTML = this.isPlaying ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>';
if (this.isPlaying) {
this.startPlayback();
} else {
this.stopPlayback();
}
},
toggleReverse: function() {
this.isReverse = !this.isReverse;
const btn = document.getElementById('tl-reverse');
btn.classList.toggle('active', this.isReverse);
btn.innerHTML = this.isReverse ?
'<i class="fas fa-forward"></i>' :
'<i class="fas fa-backward"></i>';
},
cycleSpeed: function() {
const idx = this.availableSpeeds.indexOf(this.speed);
this.speed = this.availableSpeeds[(idx + 1) % this.availableSpeeds.length];
document.getElementById('tl-speed').textContent = this.speed + 'x';
if (this.isPlaying) {
this.stopPlayback();
this.startPlayback();
}
},
startPlayback: function() {
const interval = this.baseInterval / this.speed;
this.intervalId = setInterval(() => this.nextFrame(), interval);
},
stopPlayback: function() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
},
nextFrame: function() {
if (this.isReverse) {
this.currentIndex--;
if (this.currentIndex < 0) this.currentIndex = this.imageFiles.length - 1;
} else {
this.currentIndex++;
if (this.currentIndex >= this.imageFiles.length) this.currentIndex = 0;
}
this.showFrame(this.currentIndex);
},
seekTo: function(index) {
this.currentIndex = index;
this.showFrame(index);
},
showFrame: function(index) {
const img = document.getElementById('timelapse-image');
if (img && this.imageFiles[index]) {
img.src = this.imageFiles[index];
}
this.updateSlider();
this.updateTimeDisplay();
},
updateSlider: function() {
const slider = document.getElementById('tl-slider');
if (slider) slider.value = this.currentIndex;
},
updateTimeDisplay: function() {
const display = document.getElementById('tl-time-display');
if (!display || !this.imageFiles[this.currentIndex]) return;
const filename = this.imageFiles[this.currentIndex];
const match = filename.match(/(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);
if (match) {
const [_, y, m, d, h, min, s] = match;
display.textContent = `${d}.${m}.${y} ${h}:${min}:${s}`;
}
},
backToLive: function() {
this.stopPlayback();
this.isPlaying = false;
// Live-Video wieder anzeigen
document.getElementById('timelapse-viewer').style.display = 'none';
document.getElementById('webcam-player').style.display = 'block';
document.getElementById('timelapse-button').textContent = 'Wochenzeitraffer';
// Controls verstecken
const controls = document.getElementById('timelapse-controls');
if (controls) controls.style.display = 'none';
},
show: function() {
document.getElementById('timelapse-viewer').style.display = 'block';
document.getElementById('webcam-player').style.display = 'none';
document.getElementById('daily-video-player').style.display = 'none';
const controls = document.getElementById('timelapse-controls');
if (controls) controls.style.display = 'block';
this.currentIndex = 0;
this.showFrame(0);
}
};
+108
View File
@@ -0,0 +1,108 @@
/**
* Daily Video Player - Spielt Tagesvideos im Hauptfenster ab
*/
const DailyVideoPlayer = {
currentVideo: null,
videoElement: null,
init: function() {
this.createPlayerElement();
this.setupEventListeners();
},
createPlayerElement: function() {
// Player-Container erstellen falls nicht vorhanden
if (document.getElementById('daily-video-player')) return;
const container = document.createElement('div');
container.id = 'daily-video-player';
container.style.display = 'none';
container.innerHTML = `
<video id="daily-video" controls playsinline>
<source src="" type="video/mp4">
</video>
<div class="video-player-controls">
<button id="dvp-back-live" class="tl-btn tl-back-btn">
<i class="fas fa-video"></i> Zurück zu Live
</button>
<a id="dvp-download" class="button" style="display:none;">
<i class="fas fa-download"></i> Download
</a>
</div>
`;
// Nach dem Webcam-Player einfügen
const videoContainer = document.querySelector('.video-container');
if (videoContainer) {
videoContainer.appendChild(container);
}
this.videoElement = document.getElementById('daily-video');
},
setupEventListeners: function() {
document.getElementById('dvp-back-live')?.addEventListener('click', () => this.backToLive());
// Video-Ende Event
this.videoElement?.addEventListener('ended', () => {
// Optional: Automatisch zurück zu Live
});
},
playVideo: function(videoPath, allowDownload = true) {
this.currentVideo = videoPath;
// Andere Player verstecken
document.getElementById('webcam-player').style.display = 'none';
document.getElementById('timelapse-viewer').style.display = 'none';
document.getElementById('timelapse-controls')?.style.display = 'none';
// Diesen Player anzeigen
const player = document.getElementById('daily-video-player');
player.style.display = 'block';
// Video laden
this.videoElement.src = videoPath;
this.videoElement.load();
this.videoElement.play();
// Download-Button
const downloadBtn = document.getElementById('dvp-download');
if (allowDownload && downloadBtn) {
downloadBtn.style.display = 'inline-block';
downloadBtn.href = videoPath;
downloadBtn.download = videoPath.split('/').pop();
} else if (downloadBtn) {
downloadBtn.style.display = 'none';
}
},
backToLive: function() {
// Video stoppen
if (this.videoElement) {
this.videoElement.pause();
this.videoElement.src = '';
}
// Player verstecken
document.getElementById('daily-video-player').style.display = 'none';
// Live-Stream anzeigen
document.getElementById('webcam-player').style.display = 'block';
},
// Wird vom Kalender aufgerufen
handleCalendarClick: function(videoPath, playInPlayer, allowDownload) {
if (playInPlayer) {
this.playVideo(videoPath, allowDownload);
} else {
// Nur Download
window.location.href = videoPath;
}
}
};
// Initialisierung
document.addEventListener('DOMContentLoaded', function() {
DailyVideoPlayer.init();
});