Webcam: "Kamera offline"-Overlay + Auto-Recovery des Streams
ESP32-CAM haengt sich gelegentlich auf -> Bild blieb leer ohne Hinweis. - Grabber merkt sich last_frame_ts; /api/webcam_status liefert online-Flag (online = letzter Frame < 5s her) - webcam.html pollt alle 2s und blendet ein Offline-Overlay ein - Auto-Recovery: kommt die Kamera zurueck, wird der MJPEG-Stream neu angestossen (<img> reconnectet sonst nach Abbruch nicht von selbst) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
app.py
12
app.py
@@ -437,6 +437,7 @@ class WebcamGrabber:
|
|||||||
|
|
||||||
self.latest_jpeg: bytes | None = None
|
self.latest_jpeg: bytes | None = None
|
||||||
self.frame_seq = 0
|
self.frame_seq = 0
|
||||||
|
self.last_frame_ts = 0.0 # time.time() des letzten gelieferten Frames
|
||||||
self.viewers = 0
|
self.viewers = 0
|
||||||
self.reset_flag = Event()
|
self.reset_flag = Event()
|
||||||
self.line = dict(SAVED_LINE)
|
self.line = dict(SAVED_LINE)
|
||||||
@@ -511,6 +512,7 @@ class WebcamGrabber:
|
|||||||
with self.frame_cond:
|
with self.frame_cond:
|
||||||
self.latest_jpeg = jpeg
|
self.latest_jpeg = jpeg
|
||||||
self.frame_seq += 1
|
self.frame_seq += 1
|
||||||
|
self.last_frame_ts = time.time()
|
||||||
self.frame_cond.notify_all()
|
self.frame_cond.notify_all()
|
||||||
|
|
||||||
def _clear(self):
|
def _clear(self):
|
||||||
@@ -519,6 +521,10 @@ class WebcamGrabber:
|
|||||||
self.frame_seq += 1
|
self.frame_seq += 1
|
||||||
self.frame_cond.notify_all()
|
self.frame_cond.notify_all()
|
||||||
|
|
||||||
|
def is_online(self) -> bool:
|
||||||
|
"""True, wenn zuletzt vor < 5s ein Frame kam (Kamera liefert)."""
|
||||||
|
return self.latest_jpeg is not None and (time.time() - self.last_frame_ts) < 5.0
|
||||||
|
|
||||||
# -- Hintergrund-Thread (laeuft die ganze Prozess-Lebensdauer) ----------
|
# -- Hintergrund-Thread (laeuft die ganze Prozess-Lebensdauer) ----------
|
||||||
def _run(self):
|
def _run(self):
|
||||||
state = new_state()
|
state = new_state()
|
||||||
@@ -755,6 +761,12 @@ def get_counting_line():
|
|||||||
return jsonify(get_line_from_session())
|
return jsonify(get_line_from_session())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/webcam_status", methods=["GET"])
|
||||||
|
def webcam_status():
|
||||||
|
"""Liefert, ob der Webcam-Grabber gerade Frames bekommt (Kamera online)."""
|
||||||
|
return jsonify({"online": webcam.is_online()})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/reset_count", methods=["POST"])
|
@app.route("/api/reset_count", methods=["POST"])
|
||||||
def reset_count():
|
def reset_count():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
|
|||||||
@@ -46,6 +46,27 @@
|
|||||||
.theme-toggle:hover {
|
.theme-toggle:hover {
|
||||||
border-color: var(--muted);
|
border-color: var(--muted);
|
||||||
}
|
}
|
||||||
|
.offline-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.78);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.offline-overlay.show { display: flex; }
|
||||||
|
.offline-overlay .icon { font-size: 54px; }
|
||||||
|
.offline-overlay .msg { font-size: 22px; font-weight: 600; }
|
||||||
|
.offline-overlay .sub { font-size: 14px; opacity: 0.8; }
|
||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
@@ -122,6 +143,11 @@
|
|||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<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 class="icon">📷</div>
|
||||||
|
<div class="msg">Kamera offline</div>
|
||||||
|
<div class="sub">Versuche neu zu verbinden…</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="setLineBtn">Zähllinie setzen</button>
|
<button id="setLineBtn">Zähllinie setzen</button>
|
||||||
@@ -258,6 +284,28 @@
|
|||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
// --- Kamera-Online-Status pollen + Auto-Recovery des Streams ---
|
||||||
|
const videoFeed = document.getElementById('videoFeed');
|
||||||
|
const offlineOverlay = document.getElementById('offlineOverlay');
|
||||||
|
let camWasOffline = false;
|
||||||
|
async function checkCamStatus() {
|
||||||
|
let online = false;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/webcam_status', { cache: 'no-store' });
|
||||||
|
online = (await res.json()).online;
|
||||||
|
} catch (e) {
|
||||||
|
online = false;
|
||||||
|
}
|
||||||
|
if (online && camWasOffline) {
|
||||||
|
// MJPEG-<img> reconnectet nach Stream-Abbruch nicht von selbst -> neu anstossen
|
||||||
|
videoFeed.src = "{{ url_for('webcam_feed') }}?t=" + Date.now();
|
||||||
|
}
|
||||||
|
camWasOffline = !online;
|
||||||
|
offlineOverlay.classList.toggle('show', !online);
|
||||||
|
}
|
||||||
|
checkCamStatus();
|
||||||
|
setInterval(checkCamStatus, 2000);
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
function applyThemeBtn(){var d=document.documentElement.getAttribute('data-theme')==='dark';var b=document.getElementById('themeToggle');if(b)b.textContent=d?'☀️':'🌙';}
|
function applyThemeBtn(){var d=document.documentElement.getAttribute('data-theme')==='dark';var b=document.getElementById('themeToggle');if(b)b.textContent=d?'☀️':'🌙';}
|
||||||
|
|||||||
Reference in New Issue
Block a user