Fix: Offline-Erkennung loeste Reconnect-Resets aus (Zaehlung gestoert)

Bei GRABBER_ALWAYS_ON=0 kappte kurzes Zuschauer-Aus (<img>-Reload der
Auto-Recovery) die Kameraverbindung -> Reconnect -> Tracker/Zaehlzustand
wurde zurueckgesetzt. Bei ~1 FPS riss das die Zaehlung auseinander.

- Grabber: Karenzzeit (VIEWER_GRACE_SEC, Default 15s) bevor die Kamera
  bei fehlenden Zuschauern freigegeben wird -> kein Reconnect-Churn
- Frontend: Overlay/Reload erst nach ~6s echtem Ausfall (3 Polls),
  nicht bei einzelnen langsamen Frames -> kein Verbindungs-Churn

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:53:46 +02:00
parent 356d0ce977
commit 264b2b3e3b
2 changed files with 38 additions and 10 deletions

24
app.py
View File

@@ -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. <img>-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. <img>-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

View File

@@ -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-<img> reconnectet nach Stream-Abbruch nicht von selbst -> neu anstossen
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);