188 lines
5.6 KiB
HTML
188 lines
5.6 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||
<title>Videoplayer – Display</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html, body {
|
||
width: 100%; height: 100%;
|
||
background: #000;
|
||
overflow: hidden;
|
||
font-family: system-ui, sans-serif;
|
||
}
|
||
#stage {
|
||
width: 100%; height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
#stage video,
|
||
#stage img {
|
||
width: 100%; height: 100%;
|
||
object-fit: contain;
|
||
display: none;
|
||
}
|
||
#stage audio {
|
||
display: none;
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 80%;
|
||
max-width: 400px;
|
||
}
|
||
#empty {
|
||
color: #444;
|
||
font-size: 1.1rem;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="stage">
|
||
<video id="video" muted playsinline></video>
|
||
<audio id="audio" muted controls></audio>
|
||
<img id="image" alt="">
|
||
<div id="empty">Warte auf Steuerung…</div>
|
||
</div>
|
||
<script>
|
||
const video = document.getElementById('video');
|
||
const audio = document.getElementById('audio');
|
||
const image = document.getElementById('image');
|
||
const empty = document.getElementById('empty');
|
||
|
||
let lastVersion = -1;
|
||
let lastCurrentName = null;
|
||
let audioUnlocked = false;
|
||
|
||
// AudioContext-Trick: entsperrt Audio in Chromium-basierten Browsern
|
||
(function unlockAudio() {
|
||
try {
|
||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||
ctx.resume();
|
||
const buf = ctx.createBuffer(1, 1, 22050);
|
||
const src = ctx.createBufferSource();
|
||
src.buffer = buf;
|
||
src.connect(ctx.destination);
|
||
src.start();
|
||
// Sobald der Buffer abgespielt wurde, gilt Audio als entsperrt
|
||
src.onended = () => { audioUnlocked = true; };
|
||
audioUnlocked = true;
|
||
} catch (_) {}
|
||
})();
|
||
|
||
// Klick auf die Seite entsperrt Audio endgültig
|
||
document.addEventListener('click', () => {
|
||
audioUnlocked = true;
|
||
video.muted = false;
|
||
audio.muted = false;
|
||
if (video.src && video.paused) video.play().catch(() => {});
|
||
if (audio.src && audio.paused) audio.play().catch(() => {});
|
||
}, { once: true });
|
||
|
||
function showOnly(el) {
|
||
[video, audio, image, empty].forEach(e => { e.style.display = 'none'; });
|
||
if (el) el.style.display = el === empty ? 'flex' : 'block';
|
||
}
|
||
|
||
async function tryPlay(player) {
|
||
player.muted = false;
|
||
try {
|
||
await player.play();
|
||
return true;
|
||
} catch (_) {
|
||
player.muted = true;
|
||
try {
|
||
await player.play();
|
||
return false;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function poll() {
|
||
try {
|
||
const res = await fetch('/api/state');
|
||
const state = await res.json();
|
||
const versionChanged = state.version !== lastVersion;
|
||
|
||
if (versionChanged) {
|
||
const prevVersion = lastVersion;
|
||
lastVersion = state.version;
|
||
|
||
const currentName = state.current ? state.current.name : null;
|
||
const currentChanged = currentName !== lastCurrentName;
|
||
lastCurrentName = currentName;
|
||
|
||
// Seek
|
||
if (state.seek && prevVersion >= 0 && state.current &&
|
||
(state.current.kind === 'video' || state.current.kind === 'audio')) {
|
||
const player = state.current.kind === 'video' ? video : audio;
|
||
if (player.duration && isFinite(player.duration)) {
|
||
player.currentTime = Math.max(0, Math.min(player.duration, player.currentTime + state.seek));
|
||
}
|
||
fetch('/api/seek-ack', { method: 'POST' }).catch(() => {});
|
||
}
|
||
|
||
// Neues Medium laden
|
||
if (currentChanged) {
|
||
if (state.current) {
|
||
const src = `/media/${encodeURIComponent(state.current.name)}`;
|
||
const kind = state.current.kind;
|
||
if (kind === 'video') {
|
||
video.src = src;
|
||
video.oncanplay = () => {
|
||
if (lastCurrentName === state.current?.name && state.playing) {
|
||
tryPlay(video);
|
||
}
|
||
};
|
||
showOnly(video);
|
||
} else if (kind === 'audio') {
|
||
audio.src = src;
|
||
audio.oncanplay = () => {
|
||
if (lastCurrentName === state.current?.name && state.playing) {
|
||
tryPlay(audio);
|
||
}
|
||
};
|
||
showOnly(audio);
|
||
} else {
|
||
image.src = src;
|
||
showOnly(image);
|
||
}
|
||
} else {
|
||
showOnly(empty);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Play/pause: immer ausführen
|
||
if (state.current && (state.current.kind === 'video' || state.current.kind === 'audio')) {
|
||
const player = state.current.kind === 'video' ? video : audio;
|
||
player.volume = state.volume ?? 1.0;
|
||
if (state.playing && player.src) {
|
||
tryPlay(player);
|
||
} else if (!state.playing) {
|
||
player.pause();
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function heartbeat() {
|
||
fetch('/api/display/ping', { method: 'POST' }).catch(() => {});
|
||
}
|
||
|
||
window.addEventListener('load', () => {
|
||
poll();
|
||
heartbeat();
|
||
setInterval(poll, 1500);
|
||
setInterval(heartbeat, 5000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|