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);