diff --git a/app.py b/app.py
index f5b3517..b19333f 100644
--- a/app.py
+++ b/app.py
@@ -53,6 +53,11 @@ MOTION_PIXELS = int(os.environ.get("MOTION_PIXELS", "500"))
# 0 = nur zaehlen/streamen, wenn ein Browser zuschaut (gibt ESP32-Slot frei).
# 1 = dauerhaft verbinden + zaehlen, egal ob jemand zuschaut.
GRABBER_ALWAYS_ON = os.environ.get("GRABBER_ALWAYS_ON", "0") == "1"
+# Karenzzeit: nach dem letzten Zuschauer noch so viele Sekunden weiterstreamen,
+# bevor die Kameraverbindung freigegeben wird. Verhindert, dass kurzes
+# Zuschauer-Aus (z.B.
-Reload durch die Offline-Erkennung) staendig
+# Reconnects samt Tracker-/Zaehlzustand-Reset ausloest.
+VIEWER_GRACE_SEC = float(os.environ.get("VIEWER_GRACE_SEC", "15"))
# FP16 nur auf CUDA sinnvoll -> automatisch erkennen, per Env erzwingbar.
try:
@@ -460,6 +465,7 @@ class WebcamGrabber:
self.frame_seq = 0
self.last_frame_ts = 0.0 # time.time() des letzten gelieferten Frames
self.viewers = 0
+ self._last_active_ts = 0.0 # letzter Zeitpunkt mit Zuschauer (Karenzzeit)
self.reset_flag = Event()
self.line = dict(SAVED_LINE)
@@ -498,6 +504,16 @@ class WebcamGrabber:
with self.lock:
return self.viewers
+ def _wants_stream(self) -> bool:
+ """Soll der Grabber gerade die Kamera bedienen? Mit Karenzzeit, damit
+ kurzes Zuschauer-Aus (z.B.
-Reload) die Verbindung nicht kappt."""
+ if GRABBER_ALWAYS_ON:
+ return True
+ if self._viewer_count() > 0:
+ self._last_active_ts = time.time()
+ return True
+ return (time.time() - self._last_active_ts) < VIEWER_GRACE_SEC
+
def _current_line(self):
with self.lock:
line = dict(self.line)
@@ -564,8 +580,8 @@ class WebcamGrabber:
state = new_state()
while True:
- # Ohne Always-on: nur verbinden, wenn jemand zuschaut (gibt ESP32-Slot frei).
- if not GRABBER_ALWAYS_ON and self._viewer_count() == 0:
+ # Ohne Always-on: nur verbinden, wenn jemand zuschaut (mit Karenzzeit).
+ if not self._wants_stream():
if self.latest_jpeg is not None:
self._clear()
time.sleep(0.3)
@@ -587,8 +603,8 @@ class WebcamGrabber:
buf = b""
for chunk in resp.iter_content(chunk_size=8192):
- if not GRABBER_ALWAYS_ON and self._viewer_count() == 0:
- break # letzter Viewer weg -> Verbindung freigeben
+ if not self._wants_stream():
+ break # Zuschauer (inkl. Karenz) weg -> Verbindung freigeben
if not chunk:
continue
diff --git a/templates/webcam.html b/templates/webcam.html
index 672b975..00e981a 100644
--- a/templates/webcam.html
+++ b/templates/webcam.html
@@ -288,7 +288,9 @@
// --- Kamera-Online-Status pollen + Auto-Recovery des Streams ---
const videoFeed = document.getElementById('videoFeed');
const offlineOverlay = document.getElementById('offlineOverlay');
- let camWasOffline = false;
+ let offlineStreak = 0; // aufeinanderfolgende Offline-Abfragen
+ let reloadPending = false; // erst nach echtem Ausfall den Stream neu laden
+ const OFFLINE_POLLS = 3; // ~6s -> erst dann "wirklich offline"
async function checkCamStatus() {
let online = false;
try {
@@ -297,12 +299,22 @@
} catch (e) {
online = false;
}
- if (online && camWasOffline) {
- // MJPEG-
reconnectet nach Stream-Abbruch nicht von selbst -> neu anstossen
- videoFeed.src = "{{ url_for('webcam_feed') }}?t=" + Date.now();
+ if (online) {
+ // Nur nach einem laengeren Ausfall den MJPEG-Stream neu anstossen
+ // (sonst Verbindungs-/Reconnect-Churn bei kurzen Aussetzern).
+ if (reloadPending) {
+ videoFeed.src = "{{ url_for('webcam_feed') }}?t=" + Date.now();
+ reloadPending = false;
+ }
+ offlineStreak = 0;
+ offlineOverlay.classList.remove('show');
+ } else {
+ offlineStreak++;
+ if (offlineStreak >= OFFLINE_POLLS) {
+ offlineOverlay.classList.add('show');
+ reloadPending = true;
+ }
}
- camWasOffline = !online;
- offlineOverlay.classList.toggle('show', !online);
}
checkCamStatus();
setInterval(checkCamStatus, 2000);