demo console: polled frames and the real stats field names
The camera used an MJPEG stream through the proxy and the engine's stats under names it does not use (frames/faces rather than frames_processed/faces_seen), so the picture was blank and the counter read zero. The engine re-serves its latest frame until the pipeline produces a new one, so a polled still is the same picture with none of the multipart fragility - which matters when the audience is in the room. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
@@ -230,11 +230,18 @@ async function tick(){
|
|||||||
setPill('#d-cloud','#t-cloud', s.cloud_ok ? 'ok' : 'bad', s.cloud_ok ? 'cloud connected' : 'cloud unreachable');
|
setPill('#d-cloud','#t-cloud', s.cloud_ok ? 'ok' : 'bad', s.cloud_ok ? 'cloud connected' : 'cloud unreachable');
|
||||||
$('#camname').textContent = cam ? cam.id : '';
|
$('#camname').textContent = cam ? cam.id : '';
|
||||||
|
|
||||||
if (cam && cam.connected && camStarted !== cam.id){
|
if (cam && cam.connected){
|
||||||
camStarted = cam.id;
|
if (camStarted !== cam.id){
|
||||||
$('#feed').src = '/camera.mjpeg?id=' + encodeURIComponent(cam.id);
|
camStarted = cam.id;
|
||||||
$('#feed').style.display = 'block'; $('#feednone').style.display = 'none';
|
$('#feed').style.display = 'block'; $('#feednone').style.display = 'none';
|
||||||
$('#camtag').style.display = 'block'; $('#camtag').textContent = cam.id;
|
$('#camtag').style.display = 'block';
|
||||||
|
// A polled still rather than an MJPEG stream: the engine re-serves its
|
||||||
|
// latest frame anyway, and a multipart stream through a proxy is one
|
||||||
|
// more thing to fail in front of an audience.
|
||||||
|
setInterval(() => { $('#feed').src = '/camera.jpg?id=' +
|
||||||
|
encodeURIComponent(camStarted) + '&t=' + Date.now(); }, 350);
|
||||||
|
}
|
||||||
|
$('#camtag').textContent = cam.id + (cam.tracks ? ` · ${cam.tracks} in frame` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const e = (s.chain||[])[0];
|
const e = (s.chain||[])[0];
|
||||||
|
|||||||
@@ -133,8 +133,11 @@ def watch():
|
|||||||
try:
|
try:
|
||||||
auth = engine_auth()
|
auth = engine_auth()
|
||||||
st, d = fetch(f"{ENGINE}/api/events?limit=25", headers={"authorization": auth})
|
st, d = fetch(f"{ENGINE}/api/events?limit=25", headers={"authorization": auth})
|
||||||
if st == 200 and isinstance(d, dict):
|
# The engine returns a bare list; a dict with "events" is accepted
|
||||||
for e in d.get("events", []):
|
# too so this survives either shape.
|
||||||
|
evs = d if isinstance(d, list) else (d or {}).get("events", []) if isinstance(d, dict) else []
|
||||||
|
if st == 200:
|
||||||
|
for e in evs:
|
||||||
if e.get("type") not in ("person.new", "person.seen"):
|
if e.get("type") not in ("person.new", "person.seen"):
|
||||||
continue
|
continue
|
||||||
key = f"{e.get('ts')}|{e.get('camera_id')}|{(e.get('data') or {}).get('identity_id')}"
|
key = f"{e.get('ts')}|{e.get('camera_id')}|{(e.get('data') or {}).get('identity_id')}"
|
||||||
@@ -215,7 +218,9 @@ def snapshot() -> dict:
|
|||||||
"up": isinstance(health, dict) and bool(health.get("status")),
|
"up": isinstance(health, dict) and bool(health.get("status")),
|
||||||
"model": (health or {}).get("recognition_model") if isinstance(health, dict) else None,
|
"model": (health or {}).get("recognition_model") if isinstance(health, dict) else None,
|
||||||
"cameras": [{"id": c.get("camera_id"), "connected": c.get("connected"),
|
"cameras": [{"id": c.get("camera_id"), "connected": c.get("connected"),
|
||||||
"frames": c.get("frames"), "faces": c.get("faces")} for c in cams],
|
"frames": c.get("frames_processed") or c.get("frames_total"),
|
||||||
|
"faces": c.get("faces_seen"),
|
||||||
|
"tracks": c.get("active_tracks")} for c in cams],
|
||||||
},
|
},
|
||||||
"cloud_ok": st_v == 200,
|
"cloud_ok": st_v == 200,
|
||||||
"chain": chain,
|
"chain": chain,
|
||||||
@@ -256,9 +261,26 @@ def main():
|
|||||||
return self._send(400, b'{"error":"no id"}')
|
return self._send(400, b'{"error":"no id"}')
|
||||||
st, d = cloud.call(f"/api/visitors/{vid}/history?limit=8")
|
st, d = cloud.call(f"/api/visitors/{vid}/history?limit=8")
|
||||||
return self._send(200, json.dumps({"status": st, "history": d}).encode())
|
return self._send(200, json.dumps({"status": st, "history": d}).encode())
|
||||||
if u.path == "/camera.mjpeg":
|
if u.path == "/camera.jpg":
|
||||||
|
# A polled still, not the MJPEG stream. The engine re-serves its
|
||||||
|
# latest frame until the pipeline produces a new one, so polling
|
||||||
|
# shows the same picture - and a multipart stream through a
|
||||||
|
# proxy is one more thing to fail in front of an audience.
|
||||||
cam = parse_qs(u.query).get("id", [""])[0]
|
cam = parse_qs(u.query).get("id", [""])[0]
|
||||||
return self._proxy_stream(f"{ENGINE}/api/cameras/{cam}/stream.mjpeg")
|
st, body = fetch(f"{ENGINE}/api/cameras/{cam}/frame.jpg",
|
||||||
|
headers={"authorization": engine_auth()}, timeout=15)
|
||||||
|
if st != 200 or not isinstance(body, (bytes, bytearray)):
|
||||||
|
return self._send(502, b'{"error":"no frame"}')
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("content-type", "image/jpeg")
|
||||||
|
self.send_header("cache-control", "no-store")
|
||||||
|
self.send_header("content-length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
try:
|
||||||
|
self.wfile.write(body)
|
||||||
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
|
pass
|
||||||
|
return
|
||||||
self._send(404, b'{"error":"no"}')
|
self._send(404, b'{"error":"no"}')
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user