babycam/app/templates/live.html

271 lines
6.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Babycam Live</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
background: #000;
overflow: hidden;
}
#stream {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
#no-cam {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #444;
font-family: -apple-system, sans-serif;
gap: 0.5rem;
}
#no-cam span { font-size: 3rem; }
#no-cam p { font-size: 0.9rem; }
/* Overlay erscheint bei Tap/Hover */
#overlay {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 30%, transparent 70%, rgba(0,0,0,0.5) 100%);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
body.show-ui #overlay { opacity: 1; }
#top-bar {
position: absolute;
top: 0; left: 0; right: 0;
padding: 1rem 1.25rem;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-family: -apple-system, sans-serif;
}
#top-bar .title {
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
#top-bar .dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #f44336;
animation: pulse 1.5s infinite;
}
{% if status.cam_available %}
#top-bar .dot { background: #4caf50; animation: none; }
{% endif %}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
#bottom-bar {
position: absolute;
bottom: 0; left: 0; right: 0;
padding: 1rem 1.25rem;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-family: -apple-system, sans-serif;
font-size: 0.85rem;
}
#clock { font-size: 1.1rem; font-weight: 500; letter-spacing: 0.05em; }
.btn {
background: rgba(255,255,255,0.15);
backdrop-filter: blur(8px);
border: none;
color: #fff;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.btn:hover { background: rgba(255,255,255,0.25); }
/* Fullscreen-Button */
#fs-btn {
position: absolute;
top: 1rem; right: 1.25rem;
pointer-events: all;
}
/* Alles außer fs-btn nur bei show-ui anzeigen */
#top-bar .title,
#bottom-bar {
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
body.show-ui #top-bar .title,
body.show-ui #bottom-bar {
opacity: 1;
pointer-events: all;
}
body.show-ui #fs-btn { opacity: 1; }
#fs-btn { opacity: 0; transition: opacity 0.3s; pointer-events: all; }
body.show-ui #fs-btn { opacity: 1; }
</style>
</head>
<body>
{% if status.cam_available %}
<img id="stream" src="/stream" alt="Babycam Live-Stream">
{% else %}
<div id="no-cam">
<span>📷</span>
<p>Kamera nicht verbunden</p>
</div>
{% endif %}
<div id="overlay"></div>
<div id="top-bar">
<div class="title">
<div class="dot"></div>
Babycam Live
</div>
<button class="btn" id="fs-btn" onclick="toggleFullscreen()">⛶ Vollbild</button>
</div>
<div id="bottom-bar">
<div id="clock"></div>
<button class="btn" id="audio-btn" onclick="toggleAudio()">🔇 Ton an</button>
<a class="btn" href="/">⚙ Einstellungen</a>
</div>
<script>
// UI bei Tap/Klick kurz einblenden
let hideTimer;
function showUI() {
document.body.classList.add("show-ui");
clearTimeout(hideTimer);
hideTimer = setTimeout(() => document.body.classList.remove("show-ui"), 3000);
}
document.body.addEventListener("click", showUI);
document.body.addEventListener("touchstart", showUI, { passive: true });
// Uhr
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2, "0");
const m = String(now.getMinutes()).padStart(2, "0");
const s = String(now.getSeconds()).padStart(2, "0");
document.getElementById("clock").textContent = `${h}:${m}:${s}`;
}
updateClock();
setInterval(updateClock, 1000);
// Audio via WebSocket + Web Audio API (minimale Latenz)
let audioOn = false;
let audioCtx = null;
let audioWs = null;
let nextPlayTime = 0;
const SAMPLE_RATE = 16000;
function toggleAudio() {
const btn = document.getElementById("audio-btn");
if (audioOn) {
stopAudio();
btn.textContent = "🔇 Ton an";
audioOn = false;
} else {
startAudio();
btn.textContent = "🔊 Ton aus";
audioOn = true;
}
}
function startAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE });
nextPlayTime = audioCtx.currentTime;
const wsUrl = `ws://${location.host}/audio-ws`;
audioWs = new WebSocket(wsUrl);
audioWs.binaryType = "arraybuffer";
audioWs.onmessage = (event) => {
const pcm16 = new Int16Array(event.data);
const buffer = audioCtx.createBuffer(1, pcm16.length, SAMPLE_RATE);
const data = buffer.getChannelData(0);
for (let i = 0; i < pcm16.length; i++) {
data[i] = pcm16[i] / 32768.0;
}
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
const now = audioCtx.currentTime;
// Kleine Puffer-Queue (~40ms) damit keine Lücken entstehen
if (nextPlayTime < now) nextPlayTime = now + 0.04;
source.start(nextPlayTime);
nextPlayTime += buffer.duration;
};
audioWs.onerror = () => stopAudio();
}
function stopAudio() {
if (audioWs) { audioWs.close(); audioWs = null; }
if (audioCtx) { audioCtx.close(); audioCtx = null; }
nextPlayTime = 0;
}
// Fullscreen
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen();
}
}
document.addEventListener("fullscreenchange", () => {
const btn = document.getElementById("fs-btn");
btn.textContent = document.fullscreenElement ? "✕ Vollbild beenden" : "⛶ Vollbild";
});
// Auf Handy: Landscape empfehlen
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock("landscape").catch(() => {});
}
</script>
</body>
</html>