Neues Web-Frontend mit Statusanzeige, Vorschau-Leiste und relativen Links
This commit is contained in:
1 parent
cb21f948ee
commit
aded536568
3 files changed
+202
-23
No files matched your search
@@ -3,8 +3,9 @@
|
||||
per HTTP für beliebig viele Zuschauer bereit (hinter Zoraxy / Reverse Proxy).
|
||||
|
||||
Endpunkte:
|
||||
/ Übersicht aller Kameras
|
||||
/ Übersicht aller Kameras (Web-Frontend, frontend.html)
|
||||
/<name>/ Webseite mit Livebild einer Kamera
|
||||
/status.json Status aller Kameras (vom Frontend abgefragt)
|
||||
/<name>/stream.mjpg Livestream (multipart/x-mixed-replace)
|
||||
/<name>/snapshot.jpg aktuelles Einzelbild
|
||||
/stream.mjpg Alias für die erste Kamera (Abwärtskompatibilität)
|
||||
@@ -17,6 +18,7 @@ Konfiguration über die Datei .env (neben relay.py) oder Umgebungsvariablen:
|
||||
"""
|
||||
import base64
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
@@ -48,22 +50,15 @@ AUTH_USER = os.environ.get("AUTH_USER", "")
|
||||
AUTH_PASS = os.environ.get("AUTH_PASS", "")
|
||||
BOUNDARY = "frame"
|
||||
|
||||
HEAD = ("<!doctype html><html lang=de><head><meta charset=utf-8>"
|
||||
"<meta name=viewport content='width=device-width,initial-scale=1'><title>Webcam</title>"
|
||||
"<style>html,body{margin:0;background:#111;color:#eee;font-family:sans-serif}"
|
||||
"img{display:block;width:100%}a{color:#8cf}"
|
||||
".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:8px;padding:8px}"
|
||||
"h3{margin:4px 0}</style></head><body>")
|
||||
FRONTEND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend.html")
|
||||
|
||||
|
||||
def single_page(name):
|
||||
return (HEAD + f"<img src=stream.mjpg alt='{name}'></body></html>").encode()
|
||||
|
||||
|
||||
def overview_page():
|
||||
cells = "".join(f"<div><h3><a href='{n}/'>{n}</a></h3><img src='{n}/stream.mjpg' alt='{n}'></div>"
|
||||
for n in CAMERAS)
|
||||
return (HEAD + f"<div class=grid>{cells}</div></body></html>").encode()
|
||||
def page(root, current):
|
||||
"""Frontend ausliefern; Links sind relativ, damit es auch unter einem Proxy-Prefix läuft."""
|
||||
with open(FRONTEND, encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
return (html.replace("__ROOT__", json.dumps(root))
|
||||
.replace("__CURRENT__", json.dumps(current))).encode()
|
||||
|
||||
|
||||
class Hub:
|
||||
@@ -89,6 +84,10 @@ class Hub:
|
||||
hubs = {name: Hub() for name in CAMERAS}
|
||||
|
||||
|
||||
def is_online(hub):
|
||||
return bool(hub.last) and time.time() - hub.last < 10
|
||||
|
||||
|
||||
def pull_camera(name, url, hub):
|
||||
"""Liest die Kamera dauerhaft; baut die Verbindung bei Fehlern neu auf."""
|
||||
while True:
|
||||
@@ -140,16 +139,18 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parts = [p for p in self.path.split("?")[0].split("/") if p]
|
||||
if parts == ["health"]:
|
||||
now = time.time()
|
||||
lines = [f"{n}: {'ok' if h.last and now - h.last < 10 else 'keine Frames'}"
|
||||
for n, h in hubs.items()]
|
||||
lines = [f"{n}: {'ok' if is_online(h) else 'keine Frames'}" for n, h in hubs.items()]
|
||||
ok = all(l.endswith(": ok") for l in lines)
|
||||
return self.send_bytes(200 if ok else 503, "text/plain", ("\n".join(lines) + "\n").encode())
|
||||
if not self.authorized():
|
||||
return self.send_bytes(401, "text/plain", b"Login erforderlich",
|
||||
{"WWW-Authenticate": 'Basic realm="Webcam"'})
|
||||
if not parts:
|
||||
return self.send_bytes(200, "text/html; charset=utf-8", overview_page())
|
||||
return self.send_bytes(200, "text/html; charset=utf-8", page("", ""))
|
||||
if parts == ["status.json"]:
|
||||
body = json.dumps({"cameras": [{"name": n, "online": is_online(h)}
|
||||
for n, h in hubs.items()]}).encode()
|
||||
return self.send_bytes(200, "application/json", body)
|
||||
if parts == ["stream.mjpg"] or parts == ["snapshot.jpg"]:
|
||||
parts = [next(iter(CAMERAS))] + parts # Alias: erste Kamera
|
||||
name = parts[0]
|
||||
@@ -159,8 +160,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
what = parts[1] if len(parts) == 2 else ""
|
||||
if what == "":
|
||||
if not self.path.split("?")[0].endswith("/"):
|
||||
return self.send_bytes(301, "text/plain", b"", {"Location": f"/{name}/"})
|
||||
return self.send_bytes(200, "text/html; charset=utf-8", single_page(name))
|
||||
return self.send_bytes(301, "text/plain", b"", {"Location": f"{name}/"}) # relativ: Proxy-Prefix bleibt erhalten
|
||||
return self.send_bytes(200, "text/html; charset=utf-8", page("../", name))
|
||||
if what == "snapshot.jpg":
|
||||
if hub.frame is None:
|
||||
return self.send_bytes(503, "text/plain", b"Noch kein Bild")
|
||||
|
||||
Reference in new issue
Block a user