b2in/public/_cabinet/index.html
2026-02-20 17:57:50 +01:00

840 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cabinet Digital Signage Bielefeld</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;600;700&display=swap" rel="stylesheet">
<script>
(function() {
const LOG_URL = 'https://cabinet.b2in.eu/logger.php';
// Kontext-Informationen für besseres Debugging
let appContext = {
currentVideo: null,
currentFooter: null,
videoPlaylistLength: 0,
footerContentLength: 0,
lastActivity: Date.now()
};
// Logging-Funktion mit Kontext
function sendLog(level, message, additionalData = {}) {
try {
const logData = {
level: level,
message: String(message),
timestamp: new Date().toISOString(),
context: {
...appContext,
...additionalData
},
viewport: `${window.innerWidth}x${window.innerHeight}`,
connection: navigator.onLine ? 'online' : 'offline'
};
fetch(LOG_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logData),
keepalive: true // Wichtig für Logs beim Verlassen der Seite
}).catch(() => {}); // Fehler beim Loggen ignorieren
} catch (e) {}
}
// Globale Fehler abfangen
window.onerror = function(msg, url, line, col, error) {
sendLog('FATAL', `JavaScript Error: ${msg}`, {
file: url,
line: line,
column: col,
stack: error?.stack
});
return false;
};
// Unhandled Promise Rejections (sehr wichtig für async/await!)
window.addEventListener('unhandledrejection', function(event) {
sendLog('ERROR', `Unhandled Promise Rejection: ${event.reason}`, {
promise: event.promise?.toString()
});
});
// Console.error überschreiben
const originalError = console.error;
console.error = function(...args) {
sendLog('ERROR', `Console Error: ${args.join(' ')}`);
originalError.apply(console, args);
};
// Console.warn überschreiben (für Warnungen)
const originalWarn = console.warn;
console.warn = function(...args) {
sendLog('WARNING', `Console Warning: ${args.join(' ')}`);
originalWarn.apply(console, args);
};
// Resource Loading Errors (z.B. Videos, Bilder)
window.addEventListener('error', function(event) {
if (event.target !== window) {
const element = event.target;
const tagName = element.tagName;
const src = element.src || element.href;
sendLog('ERROR', `Resource Failed to Load: ${tagName}`, {
src: src,
type: tagName
});
}
}, true); // useCapture = true, um alle Events zu fangen
// Online/Offline Status überwachen
window.addEventListener('online', () => {
sendLog('INFO', 'Connection restored');
});
window.addEventListener('offline', () => {
sendLog('WARNING', 'Connection lost');
});
// Heartbeat: Alle 5 Minuten ein "alive" Signal senden
setInterval(() => {
sendLog('INFO', 'Heartbeat - Display is running', {
uptime: Math.floor((Date.now() - appContext.lastActivity) / 1000) + 's'
});
}, 5 * 60 * 1000); // Alle 5 Minuten
// Initial Log beim Start
sendLog('INFO', 'Display started', {
userAgent: navigator.userAgent,
screen: `${screen.width}x${screen.height}`,
url: window.location.href
});
// Export für andere Scripts
window.displayLogger = {
log: (msg, data) => sendLog('INFO', msg, data),
warn: (msg, data) => sendLog('WARNING', msg, data),
error: (msg, data) => sendLog('ERROR', msg, data),
setContext: (key, value) => { appContext[key] = value; }
};
})();
</script>
<style>
/* --- GRUNDGERÜST --- */
body, html {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
}
#main-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
/* Seitenverhältnis 9:16 (1080:1920) beibehalten */
aspect-ratio: 9 / 16;
}
/* Wenn Bildschirm breiter als 9:16 ist, nach Höhe skalieren */
@media (min-aspect-ratio: 9/16) {
#main-container {
width: auto;
height: 100vh;
}
}
/* Wenn Bildschirm höher als 9:16 ist, nach Breite skalieren */
@media (max-aspect-ratio: 9/16) {
#main-container {
width: 100vw;
height: auto;
}
}
/* --- VIDEO BEREICH (Oben) --- */
#video-wrapper {
flex-grow: 1;
position: relative;
background: #000;
overflow: hidden;
}
#video-player {
width: 100%;
height: 100%;
object-fit: cover; /* Video füllt den Bereich randlos */
object-position: center 15%; /* Fallback-Position, wird per JavaScript überschrieben */
display: block;
/* Performance-Optimierungen für Video */
will-change: transform; /* Hint für Browser-Optimierung */
transform: translateZ(0); /* Hardware-Beschleunigung aktivieren */
backface-visibility: hidden; /* Reduziert Rendering-Last */
-webkit-backface-visibility: hidden;
}
/* --- FOOTER BEREICH (Unten - ca. 16.67% der Höhe = 320px bei 1920px) --- */
#footer {
height: 9.67vh;
min-height: 100px;
background-color: #1a1a1a; /* Dunkelgrau */
color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px; /* 60px bei 1080px Breite */
box-sizing: border-box;
font-size: 10px;
position: relative;
}
/* Progress Bar am oberen Rand des Footers */
#progress-bar {
position: absolute;
top: 0;
left: 0;
height: 3px;
background-color: #009FE3; /* Cabinet Blau */
width: 0%;
transition: none;
}
#progress-bar.animate {
animation: progressAnimation 30s linear;
}
@keyframes progressAnimation {
from {
width: 0%;
}
to {
width: 100%;
}
}
/* --- INHALTE IM FOOTER --- */
.cta-text-container {
width: 75%;
opacity: 1;
transition: opacity 1s ease-in-out;
}
.cta-headline {
font-size: 2.0em; /* Relativ zur Footer-Schriftgröße */
font-weight: 300;
margin-bottom: 0.3em;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #bbb;
}
.cta-subline {
font-size: 2.4em; /* Relativ zur Footer-Schriftgröße */
font-weight: 700;
line-height: 1.1;
}
.qr-container {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
opacity: 1;
transition: opacity 1s ease-in-out;
}
.qr-code-img {
width: 8em; /* Relativ zur Footer-Schriftgröße */
height: auto;
max-width: 100px;
aspect-ratio: 1 / 1; /* Erzwingt quadratische Form */
object-fit: contain;
background-color: white; /* Weißer Hintergrund für Lesbarkeit */
padding: 0.4em;
border-radius: 0.6em;
box-sizing: border-box;
}
.scan-hint {
margin-top: 0.8em;
font-size: 1.3em; /* Relativ zur Footer-Schriftgröße */
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: #009FE3; /* Akzentfarbe */
}
/* Hilfsklasse für den Überblend-Effekt */
.fade-out {
opacity: 0;
}
/* Loading Indicator */
.loading {
text-align: center;
padding: 2em;
color: #fff;
}
</style>
</head>
<body>
<div id="main-container">
<!-- VIDEO BEREICH -->
<div id="video-wrapper">
<!-- Videos werden hier geladen. 'muted' ist oft nötig für Autoplay -->
<video id="video-player" autoplay muted playsinline></video>
</div>
<!-- FOOTER BEREICH -->
<div id="footer">
<div id="progress-bar"></div>
<div class="cta-text-container" id="text-area">
<div class="cta-headline" id="headline">LADEN...</div>
<div class="cta-subline" id="subline">Inhalte werden geladen</div>
</div>
<div class="qr-container" id="qr-area">
<img src="" id="qr-image" class="qr-code-img" alt="QR Code">
</div>
</div>
</div>
<script>
/* ==============================================
KONFIGURATION WIRD DYNAMISCH GELADEN
============================================== */
let videoPlaylist = [];
let footerContent = [];
let footerContentLength = 0;
// Basis-URL für Assets und API (b2in.eu Server)
const BASE_URL = 'https://b2in.eu';
// API-URL für die Konfiguration (CORS ist aktiviert für cabinet.b2in.eu)
const API_URL = BASE_URL + '/api/display/config';
/* ==============================================
KONFIGURATION LADEN
============================================== */
async function loadConfiguration() {
try {
window.displayLogger?.log('Lade Konfiguration...', { url: API_URL });
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const config = await response.json();
videoPlaylist = config.videoPlaylist || [];
footerContent = config.footerContent || [];
window.displayLogger?.setContext('videoPlaylistLength', videoPlaylist.length);
window.displayLogger?.setContext('footerContentLength', footerContent.length);
window.displayLogger?.log('Konfiguration erfolgreich geladen', {
videos: videoPlaylist.length,
footerItems: footerContent.length
});
console.log('Konfiguration geladen:', config);
// Überprüfe, ob Videos vorhanden sind
if (videoPlaylist.length === 0) {
console.warn('Keine Videos in der Playlist vorhanden');
window.displayLogger?.warn('Keine Videos in Playlist');
document.getElementById('headline').innerText = 'KEINE VIDEOS';
document.getElementById('subline').innerText = 'Bitte fügen Sie Videos im CMS hinzu';
return false;
}
// Überprüfe, ob Footer-Inhalte vorhanden sind
if (footerContent.length === 0) {
console.warn('Keine Footer-Inhalte vorhanden - Footer wird ausgeblendet');
footerContentLength = 0;
// Footer ausblenden
const footer = document.getElementById('footer');
if (footer) {
footer.style.display = 'none';
}
// Video-Wrapper auf 100% Höhe setzen
const videoWrapper = document.getElementById('video-wrapper');
if (videoWrapper) {
videoWrapper.style.flexGrow = '1';
videoWrapper.style.height = '100%';
}
} else {
// Footer anzeigen, falls er zuvor ausgeblendet wurde
const footer = document.getElementById('footer');
if (footer) {
footer.style.display = 'flex';
}
footerContentLength = 1;
}
return true;
} catch (error) {
console.error('Fehler beim Laden der Konfiguration:', error);
window.displayLogger?.error('Konfiguration konnte nicht geladen werden', {
error: error.message,
stack: error.stack
});
document.getElementById('headline').innerText = 'FEHLER';
document.getElementById('subline').innerText = 'Konfiguration konnte nicht geladen werden';
return false;
}
}
/* ==============================================
PROGRAMM-LOGIK
============================================== */
// --- ROBUSTER VIDEO PLAYER MIT MEMORY-MANAGEMENT ---
const videoElement = document.getElementById('video-player');
let currentVideoIndex = 0;
let videoStartTimeout = null;
let videoWatchdogInterval = null;
let lastVideoTime = 0;
let videoStuckCount = 0;
let consecutiveErrors = 0;
const MAX_CONSECUTIVE_ERRORS = 3;
const VIDEO_START_TIMEOUT = 10000; // 10 Sekunden
const VIDEO_WATCHDOG_INTERVAL = 5000; // Alle 5 Sekunden prüfen
// Video-Element optimieren für Memory-Management
videoElement.setAttribute('preload', 'metadata'); // Nur Metadaten vorladen, nicht ganzes Video
function cleanupVideo() {
// Wichtig: Stoppt Video und gibt Speicher frei
try {
videoElement.pause();
videoElement.removeAttribute('src');
videoElement.load(); // Triggert Garbage Collection des alten Videos
// Timeouts clearen
if (videoStartTimeout) {
clearTimeout(videoStartTimeout);
videoStartTimeout = null;
}
window.displayLogger?.log('Video cleanup durchgeführt');
} catch (e) {
window.displayLogger?.error('Video cleanup error', { error: e.message });
}
}
function playNextVideo() {
if (videoPlaylist.length === 0) return;
// Watchdog zurücksetzen
lastVideoTime = 0;
videoStuckCount = 0;
const video = videoPlaylist[currentVideoIndex];
const videoSrc = BASE_URL + "/_cabinet/" + video.src;
// Kontext aktualisieren
window.displayLogger?.setContext('currentVideo', video.src);
window.displayLogger?.setContext('currentVideoIndex', currentVideoIndex);
// WICHTIG: Altes Video cleanup BEVOR neues geladen wird
cleanupVideo();
// Kleiner Delay um Cleanup abzuschließen
setTimeout(() => {
try {
// Neues Video laden
videoElement.src = videoSrc;
if(footerContentLength !== 0 && video.position !== undefined) {
videoElement.style.objectPosition = `center ${video.position}%`;
}
// Timeout für Video-Start
videoStartTimeout = setTimeout(() => {
window.displayLogger?.error('Video start timeout', {
video: video.src,
timeout: VIDEO_START_TIMEOUT
});
// Nächstes Video probieren
skipToNextVideo('timeout');
}, VIDEO_START_TIMEOUT);
// Video abspielen
videoElement.play()
.then(() => {
window.displayLogger?.log(`Video started: ${video.src}`);
consecutiveErrors = 0; // Erfolg → Error-Counter zurücksetzen
// Start-Timeout clearen
if (videoStartTimeout) {
clearTimeout(videoStartTimeout);
videoStartTimeout = null;
}
})
.catch(e => {
console.log("Autoplay blocked/failed", e);
window.displayLogger?.error(`Video play failed: ${video.src}`, {
error: e.message
});
consecutiveErrors++;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
window.displayLogger?.error('Zu viele aufeinanderfolgende Fehler', {
count: consecutiveErrors
});
// Seite nach 30 Sekunden neu laden
setTimeout(() => location.reload(), 30000);
} else {
// Nächstes Video probieren
skipToNextVideo('play_failed');
}
});
} catch (e) {
window.displayLogger?.error('Exception beim Video-Laden', {
error: e.message,
stack: e.stack
});
skipToNextVideo('exception');
}
}, 100); // 100ms Delay für Cleanup
// Index weiterschalten
currentVideoIndex++;
if (currentVideoIndex >= videoPlaylist.length) {
currentVideoIndex = 0;
// Playlist-Loop abgeschlossen → Log für Monitoring
window.displayLogger?.log('Playlist-Loop abgeschlossen, starte von vorne');
}
}
function skipToNextVideo(reason) {
window.displayLogger?.warn('Überspringe zum nächsten Video', { reason: reason });
playNextVideo();
}
// Video Watchdog: Prüft ob Video wirklich läuft
function startVideoWatchdog() {
if (videoWatchdogInterval) {
clearInterval(videoWatchdogInterval);
}
videoWatchdogInterval = setInterval(() => {
if (videoPlaylist.length === 0) return;
const currentTime = videoElement.currentTime;
const isPaused = videoElement.paused;
const hasEnded = videoElement.ended;
const isStuck = (currentTime === lastVideoTime && !isPaused && !hasEnded);
// Debug-Log
if (isStuck) {
videoStuckCount++;
window.displayLogger?.warn('Video scheint stecken geblieben zu sein', {
currentTime: currentTime,
isPaused: isPaused,
hasEnded: hasEnded,
stuckCount: videoStuckCount,
src: videoElement.src
});
// Wenn 2x hintereinander stuck → Recovery
if (videoStuckCount >= 2) {
window.displayLogger?.error('Video definitiv stuck - starte nächstes', {
currentTime: currentTime,
src: videoElement.src
});
skipToNextVideo('watchdog_stuck');
}
} else {
// Video läuft normal → Counter zurücksetzen
if (videoStuckCount > 0) {
window.displayLogger?.log('Video läuft wieder normal');
}
videoStuckCount = 0;
}
lastVideoTime = currentTime;
}, VIDEO_WATCHDOG_INTERVAL);
}
// Video Events
videoElement.addEventListener('ended', () => {
window.displayLogger?.log('Video ended', {
src: videoElement.src
});
playNextVideo();
});
videoElement.addEventListener('error', (e) => {
const error = videoElement.error;
const errorCode = error?.code;
const errorMessage = {
1: 'MEDIA_ERR_ABORTED',
2: 'MEDIA_ERR_NETWORK',
3: 'MEDIA_ERR_DECODE',
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED'
}[errorCode] || 'UNKNOWN';
window.displayLogger?.error('Video Error Event', {
code: errorCode,
message: error?.message,
src: videoElement.src,
mediaError: errorMessage
});
// Bei Fehler → Nächstes Video
consecutiveErrors++;
skipToNextVideo(`error_${errorMessage}`);
});
videoElement.addEventListener('stalled', () => {
window.displayLogger?.warn('Video stalled (buffering)', {
src: videoElement.src,
currentTime: videoElement.currentTime
});
});
videoElement.addEventListener('waiting', () => {
window.displayLogger?.warn('Video waiting (buffering)', {
src: videoElement.src,
currentTime: videoElement.currentTime
});
});
videoElement.addEventListener('playing', () => {
window.displayLogger?.log('Video playing event', {
src: videoElement.src,
currentTime: videoElement.currentTime
});
});
videoElement.addEventListener('canplay', () => {
window.displayLogger?.log('Video canplay event', {
src: videoElement.src
});
});
// --- FOOTER ROTATION LOGIC ---
let currentFooterIndex = 0;
const textArea = document.getElementById('text-area');
const qrArea = document.getElementById('qr-area');
const headlineEl = document.getElementById('headline');
const sublineEl = document.getElementById('subline');
const qrImageEl = document.getElementById('qr-image');
const progressBar = document.getElementById('progress-bar');
function restartProgressBar() {
// Animation zurücksetzen und neu starten
progressBar.classList.remove('animate');
void progressBar.offsetWidth; // Force reflow
progressBar.classList.add('animate');
}
function updateFooter() {
if (footerContent.length === 0) return;
// 1. Ausblenden
textArea.classList.add('fade-out');
qrArea.classList.add('fade-out');
// Progress Bar neu starten
restartProgressBar();
// 2. Warten, Inhalt tauschen, Einblenden
setTimeout(() => {
const content = footerContent[currentFooterIndex];
// Kontext aktualisieren
window.displayLogger?.setContext('currentFooter', currentFooterIndex);
// Text setzen
headlineEl.innerText = content.headline;
sublineEl.innerText = content.subline;
// QR Code nur generieren wenn URL vorhanden
if (content.url) {
// QR Code generieren (API Aufruf)
const qrSize = "300x300";
const qrColor = "000000"; // Schwarz
const qrBg = "ffffff"; // Weiß
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${qrSize}&color=${qrColor}&bgcolor=${qrBg}&margin=10&data=${encodeURIComponent(content.url)}`;
qrImageEl.src = qrUrl;
qrArea.style.display = 'flex'; // QR-Bereich anzeigen
} else {
// Kein QR-Code - QR-Bereich ausblenden
qrArea.style.display = 'none';
// Text-Container auf volle Breite
textArea.style.width = '100%';
}
// Index weiterschalten
currentFooterIndex++;
if (currentFooterIndex >= footerContent.length) {
currentFooterIndex = 0;
}
// 3. Einblenden
if (content.url) {
qrImageEl.onload = () => {
textArea.classList.remove('fade-out');
qrArea.classList.remove('fade-out');
};
}
// Fallback falls Bild sofort da ist (Cache) oder kein QR-Code
setTimeout(() => {
textArea.classList.remove('fade-out');
if (content.url) {
qrArea.classList.remove('fade-out');
}
}, 100);
}, 1000); // 1 Sekunde für Fade-Out Animation
}
/* ==============================================
MEMORY MANAGEMENT & PERFORMANCE
============================================== */
// Memory-Optimierung: Regelmäßig Browser aufräumen
function performMemoryOptimization() {
try {
// Performance-Metriken loggen falls verfügbar
if (performance.memory) {
const memUsed = Math.round(performance.memory.usedJSHeapSize / 1048576);
const memLimit = Math.round(performance.memory.jsHeapSizeLimit / 1048576);
const memPercent = Math.round((memUsed / memLimit) * 100);
window.displayLogger?.log('Memory Status', {
usedMB: memUsed,
limitMB: memLimit,
percentUsed: memPercent
});
// Warnung wenn Speicher über 80%
if (memPercent > 80) {
window.displayLogger?.warn('Hohe Speicherauslastung', {
percentUsed: memPercent,
usedMB: memUsed
});
}
}
// Cache-Infos loggen
const cacheInfo = {
videoBuffered: videoElement.buffered.length,
videoDuration: videoElement.duration,
videoReadyState: videoElement.readyState
};
window.displayLogger?.log('Video Cache Status', cacheInfo);
} catch (e) {
window.displayLogger?.error('Memory optimization error', {
error: e.message
});
}
}
// Automatischer Page-Reload bei kritischen Problemen (Failsafe)
let criticalErrorCount = 0;
function checkCriticalErrors() {
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
criticalErrorCount++;
window.displayLogger?.error('Kritischer Zustand erkannt', {
consecutiveErrors: consecutiveErrors,
criticalErrorCount: criticalErrorCount
});
if (criticalErrorCount >= 3) {
window.displayLogger?.error('Zu viele kritische Fehler - Seite wird neu geladen');
setTimeout(() => location.reload(), 5000);
}
} else {
criticalErrorCount = 0; // Zurücksetzen wenn alles normal läuft
}
}
/* ==============================================
INITIALISIERUNG
============================================== */
async function initialize() {
const success = await loadConfiguration();
if (success && videoPlaylist.length > 0) {
// Start Video
playNextVideo();
// Start Video Watchdog (überwacht ob Videos laufen)
startVideoWatchdog();
window.displayLogger?.log('Video Watchdog gestartet');
// Start Footer Loop
if (footerContent.length > 0) {
updateFooter();
setInterval(updateFooter, 30000); // Alle 30.000 ms (30 sek) wechseln
// Progress Bar initial starten
restartProgressBar();
}
// Memory-Optimierung alle 10 Minuten
setInterval(performMemoryOptimization, 10 * 60 * 1000);
window.displayLogger?.log('Memory Optimizer gestartet (alle 10 Min)');
// Critical Error Check alle 30 Sekunden
setInterval(checkCriticalErrors, 30 * 1000);
// Initial Memory Check nach 30 Sekunden
setTimeout(performMemoryOptimization, 30000);
}
}
// Beim Laden der Seite initialisieren
initialize();
// Auto-Reload alle 5 Minuten, um neue Inhalte zu laden
setInterval(async () => {
console.log('Prüfe auf neue Konfiguration...');
const oldFooterCount = footerContent.length;
await loadConfiguration();
// Wenn Footer-Inhalte hinzugefügt oder entfernt wurden, Seite neu laden
if ((oldFooterCount === 0 && footerContent.length > 0) ||
(oldFooterCount > 0 && footerContent.length === 0)) {
console.log('Footer-Status hat sich geändert - Seite wird neu geladen');
location.reload();
}
}, 5 * 60 * 1000); // 5 Minuten
// Präventiver Page-Reload alle 6 Stunden (verhindert Memory-Leaks über lange Zeit)
setTimeout(() => {
window.displayLogger?.log('Präventiver Reload nach 6 Stunden');
location.reload();
}, 6 * 60 * 60 * 1000); // 6 Stunden
</script>
</body>
</html>