babycam/app/templates/live.html

296 lines
7.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=yes">
<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>
<button class="btn" onclick="systemShutdown()" style="background:rgba(244,67,54,0.25);"></button>
</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 {
btn.textContent = "⏳ Verbinde…";
startAudio();
audioOn = true;
}
}
function startAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE });
// Android/Chrome suspendiert AudioContext standardmäßig → explizit resumem
audioCtx.resume().then(() => {
nextPlayTime = audioCtx.currentTime;
connectWs();
});
}
function connectWs() {
// ws:// oder wss:// je nach Seitenprotokoll
const proto = location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${proto}://${location.host}/audio-ws`;
audioWs = new WebSocket(wsUrl);
audioWs.binaryType = "arraybuffer";
audioWs.onopen = () => {
document.getElementById("audio-btn").textContent = "🔊 Ton aus";
};
audioWs.onmessage = (event) => {
if (!audioCtx) return;
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;
if (nextPlayTime < now) nextPlayTime = now + 0.04;
source.start(nextPlayTime);
nextPlayTime += buffer.duration;
};
audioWs.onerror = () => stopAudio();
audioWs.onclose = () => {
if (audioOn) stopAudio();
document.getElementById("audio-btn").textContent = "🔇 Ton an";
};
}
function stopAudio() {
if (audioWs) { audioWs.close(); audioWs = null; }
if (audioCtx) { audioCtx.close(); audioCtx = null; }
nextPlayTime = 0;
}
// Shutdown
async function systemShutdown() {
if (!confirm("Pi ausschalten?")) return;
await fetch("/system/shutdown", { method: "POST" });
document.body.innerHTML = '<div style="color:#fff;font-family:sans-serif;display:flex;height:100vh;align-items:center;justify-content:center;font-size:1.2rem;">Pi wird heruntergefahren…</div>';
}
// 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>