Webcam: Button zum Freigeben/Uebernehmen der Kamera (ESP32-Slot)
Trotz GRABBER_ALWAYS_ON=1 kann der ESP32-Slot per Button manuell
freigegeben werden (z.B. um die Cam woanders direkt zu oeffnen).
Freigegeben = Slot frei + Zaehlung pausiert; uebernehmen = wieder zaehlen.
- Grabber: paused-Flag, hat Vorrang vor ALWAYS_ON/Karenzzeit
- /api/grabber_toggle + paused-Feld in /api/webcam_status
- Button + eigener Overlay-Text ("Kamera freigegeben") im Frontend
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
19
app.py
19
app.py
@@ -466,6 +466,7 @@ class WebcamGrabber:
|
|||||||
self.last_frame_ts = 0.0 # time.time() des letzten gelieferten Frames
|
self.last_frame_ts = 0.0 # time.time() des letzten gelieferten Frames
|
||||||
self.viewers = 0
|
self.viewers = 0
|
||||||
self._last_active_ts = 0.0 # letzter Zeitpunkt mit Zuschauer (Karenzzeit)
|
self._last_active_ts = 0.0 # letzter Zeitpunkt mit Zuschauer (Karenzzeit)
|
||||||
|
self.paused = False # manuell freigegeben -> ESP32-Slot freigeben
|
||||||
self.reset_flag = Event()
|
self.reset_flag = Event()
|
||||||
self.line = dict(SAVED_LINE)
|
self.line = dict(SAVED_LINE)
|
||||||
|
|
||||||
@@ -507,6 +508,8 @@ class WebcamGrabber:
|
|||||||
def _wants_stream(self) -> bool:
|
def _wants_stream(self) -> bool:
|
||||||
"""Soll der Grabber gerade die Kamera bedienen? Mit Karenzzeit, damit
|
"""Soll der Grabber gerade die Kamera bedienen? Mit Karenzzeit, damit
|
||||||
kurzes Zuschauer-Aus (z.B. <img>-Reload) die Verbindung nicht kappt."""
|
kurzes Zuschauer-Aus (z.B. <img>-Reload) die Verbindung nicht kappt."""
|
||||||
|
if self.paused:
|
||||||
|
return False # manuell freigegeben -> ESP32-Slot freigeben
|
||||||
if GRABBER_ALWAYS_ON:
|
if GRABBER_ALWAYS_ON:
|
||||||
return True
|
return True
|
||||||
if self._viewer_count() > 0:
|
if self._viewer_count() > 0:
|
||||||
@@ -514,6 +517,12 @@ class WebcamGrabber:
|
|||||||
return True
|
return True
|
||||||
return (time.time() - self._last_active_ts) < VIEWER_GRACE_SEC
|
return (time.time() - self._last_active_ts) < VIEWER_GRACE_SEC
|
||||||
|
|
||||||
|
def set_paused(self, paused: bool) -> bool:
|
||||||
|
"""Grabber manuell pausieren (Kamera freigeben) bzw. wieder aufnehmen."""
|
||||||
|
self.paused = bool(paused)
|
||||||
|
print(f"[webcam-grabber] {'pausiert (Slot frei)' if self.paused else 'aktiv'}", flush=True)
|
||||||
|
return self.paused
|
||||||
|
|
||||||
def _current_line(self):
|
def _current_line(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
line = dict(self.line)
|
line = dict(self.line)
|
||||||
@@ -817,7 +826,15 @@ def get_counting_line():
|
|||||||
@app.route("/api/webcam_status", methods=["GET"])
|
@app.route("/api/webcam_status", methods=["GET"])
|
||||||
def webcam_status():
|
def webcam_status():
|
||||||
"""Liefert, ob der Webcam-Grabber gerade Frames bekommt (Kamera online)."""
|
"""Liefert, ob der Webcam-Grabber gerade Frames bekommt (Kamera online)."""
|
||||||
return jsonify({"online": webcam.is_online()})
|
return jsonify({"online": webcam.is_online(), "paused": webcam.paused})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/grabber_toggle", methods=["POST"])
|
||||||
|
def grabber_toggle():
|
||||||
|
"""Grabber pausieren/aufnehmen: pausiert gibt den ESP32-Slot frei
|
||||||
|
(Zaehlung aus), aktiv verbindet wieder zur Kamera (Zaehlung an)."""
|
||||||
|
paused = webcam.set_paused(not webcam.paused)
|
||||||
|
return jsonify({"paused": paused})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/reset_count", methods=["POST"])
|
@app.route("/api/reset_count", methods=["POST"])
|
||||||
|
|||||||
@@ -144,14 +144,15 @@
|
|||||||
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
|
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
|
||||||
<canvas id="lineCanvas"></canvas>
|
<canvas id="lineCanvas"></canvas>
|
||||||
<div id="offlineOverlay" class="offline-overlay">
|
<div id="offlineOverlay" class="offline-overlay">
|
||||||
<div class="icon">📷</div>
|
<div class="icon" id="ovIcon">📷</div>
|
||||||
<div class="msg">Kamera offline</div>
|
<div class="msg" id="ovMsg">Kamera offline</div>
|
||||||
<div class="sub">Versuche neu zu verbinden…</div>
|
<div class="sub" id="ovSub">Versuche neu zu verbinden…</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="setLineBtn">Zähllinie setzen</button>
|
<button id="setLineBtn">Zähllinie setzen</button>
|
||||||
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
||||||
|
<button id="grabberBtn">Kamera freigeben</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="info" id="infoText">Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.</div>
|
<div class="info" id="infoText">Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.</div>
|
||||||
<a href="/">Back to Home</a>
|
<a href="/">Back to Home</a>
|
||||||
@@ -288,20 +289,36 @@
|
|||||||
// --- Kamera-Online-Status pollen + Auto-Recovery des Streams ---
|
// --- Kamera-Online-Status pollen + Auto-Recovery des Streams ---
|
||||||
const videoFeed = document.getElementById('videoFeed');
|
const videoFeed = document.getElementById('videoFeed');
|
||||||
const offlineOverlay = document.getElementById('offlineOverlay');
|
const offlineOverlay = document.getElementById('offlineOverlay');
|
||||||
|
const ovIcon = document.getElementById('ovIcon');
|
||||||
|
const ovMsg = document.getElementById('ovMsg');
|
||||||
|
const ovSub = document.getElementById('ovSub');
|
||||||
|
const grabberBtn = document.getElementById('grabberBtn');
|
||||||
let offlineStreak = 0; // aufeinanderfolgende Offline-Abfragen
|
let offlineStreak = 0; // aufeinanderfolgende Offline-Abfragen
|
||||||
let reloadPending = false; // erst nach echtem Ausfall den Stream neu laden
|
let reloadPending = false; // erst nach echtem Ausfall den Stream neu laden
|
||||||
const OFFLINE_POLLS = 3; // ~6s -> erst dann "wirklich offline"
|
const OFFLINE_POLLS = 3; // ~6s -> erst dann "wirklich offline"
|
||||||
|
|
||||||
|
function showOverlay(icon, msg, sub) {
|
||||||
|
ovIcon.textContent = icon; ovMsg.textContent = msg; ovSub.textContent = sub;
|
||||||
|
offlineOverlay.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
async function checkCamStatus() {
|
async function checkCamStatus() {
|
||||||
let online = false;
|
let online = false, paused = false;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/webcam_status', { cache: 'no-store' });
|
const d = await (await fetch('/api/webcam_status', { cache: 'no-store' })).json();
|
||||||
online = (await res.json()).online;
|
online = d.online; paused = d.paused;
|
||||||
} catch (e) {
|
} catch (e) { online = false; }
|
||||||
online = false;
|
|
||||||
|
grabberBtn.textContent = paused ? 'Kamera übernehmen' : 'Kamera freigeben';
|
||||||
|
grabberBtn.classList.toggle('active', paused);
|
||||||
|
|
||||||
|
if (paused) {
|
||||||
|
showOverlay('⏸️', 'Kamera freigegeben', 'Slot frei für anderen Zugriff – Zählung pausiert');
|
||||||
|
offlineStreak = 0;
|
||||||
|
reloadPending = true; // beim Wiederaufnehmen Stream neu laden
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (online) {
|
if (online) {
|
||||||
// Nur nach einem laengeren Ausfall den MJPEG-Stream neu anstossen
|
|
||||||
// (sonst Verbindungs-/Reconnect-Churn bei kurzen Aussetzern).
|
|
||||||
if (reloadPending) {
|
if (reloadPending) {
|
||||||
videoFeed.src = "{{ url_for('webcam_feed') }}?t=" + Date.now();
|
videoFeed.src = "{{ url_for('webcam_feed') }}?t=" + Date.now();
|
||||||
reloadPending = false;
|
reloadPending = false;
|
||||||
@@ -311,11 +328,21 @@
|
|||||||
} else {
|
} else {
|
||||||
offlineStreak++;
|
offlineStreak++;
|
||||||
if (offlineStreak >= OFFLINE_POLLS) {
|
if (offlineStreak >= OFFLINE_POLLS) {
|
||||||
offlineOverlay.classList.add('show');
|
showOverlay('📷', 'Kamera offline', 'Versuche neu zu verbinden…');
|
||||||
reloadPending = true;
|
reloadPending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grabberBtn.addEventListener('click', async () => {
|
||||||
|
grabberBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
await fetch('/api/grabber_toggle', { method: 'POST' });
|
||||||
|
} catch (e) { /* ignorieren */ }
|
||||||
|
grabberBtn.disabled = false;
|
||||||
|
checkCamStatus();
|
||||||
|
});
|
||||||
|
|
||||||
checkCamStatus();
|
checkCamStatus();
|
||||||
setInterval(checkCamStatus, 2000);
|
setInterval(checkCamStatus, 2000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user