fix : live view buffering
This commit is contained in:
1 parent
af2854d771
commit
314b598c66
4 files changed
+73
-24
No files matched your search
@@ -193,7 +193,7 @@ sudo systemctl start zenai-kpc-counter zenai-kpc-dashboard
|
||||
| Counter won't start | `journalctl -u zenai-kpc-counter`; verify `MODEL_PATH` exists; RKNN drivers installed |
|
||||
| No RTSP frames | Ping camera; test with `ffplay <SOURCE>`; check `OPENCV_FFMPEG_CAPTURE_OPTIONS` |
|
||||
| Dashboard shows 0 count | `STATE_FILE` in `.env` must match between counter and dashboard; check file exists |
|
||||
| Live video blank | `LIVE_STREAM_ENABLED=true`; path matches `LIVE_STREAM_FRAME_PATH` in both processes |
|
||||
| Live video blank | `LIVE_STREAM_ENABLED=true`; path matches `LIVE_STREAM_FRAME_PATH` in both processes; if using nginx, set `proxy_read_timeout` (see §7) |
|
||||
| Wrong counts | Tune `LINE_Y1_FRAC`/`LINE_Y2_FRAC`, `CONF`, ByteTrack thresholds; enable `DEBUG_TRACKING=true` temporarily |
|
||||
| Service keeps restarting | `journalctl -u zenai-kpc-counter -e`; often missing model, bad RTSP URL, or venv not created |
|
||||
|
||||
@@ -222,7 +222,9 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_buffering off; # needed for /api/live-video MJPEG stream
|
||||
proxy_buffering off; # required for /api/live-video MJPEG stream
|
||||
proxy_read_timeout 3600s; # keep long-lived MJPEG connections open
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+22
-14
@@ -44,32 +44,40 @@ DASHBOARD_PORT = int(os.getenv("DASHBOARD_PORT", "5000"))
|
||||
DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
|
||||
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true"
|
||||
|
||||
_JPEG_SOI = b"\xff\xd8"
|
||||
_JPEG_EOI = b"\xff\xd9"
|
||||
_MIN_JPEG_BYTES = 128
|
||||
|
||||
|
||||
def _is_valid_jpeg(data):
|
||||
return (
|
||||
data
|
||||
and len(data) >= _MIN_JPEG_BYTES
|
||||
and data.startswith(_JPEG_SOI)
|
||||
and data.endswith(_JPEG_EOI)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/live-video")
|
||||
def api_live_video():
|
||||
if not os.path.isfile(LIVE_STREAM_FRAME_PATH):
|
||||
return jsonify({"success": False, "error": "Live stream frame not available yet"}), 503
|
||||
|
||||
def generate():
|
||||
consecutive_fails = 0
|
||||
MAX_FAILS = 30
|
||||
while True:
|
||||
try:
|
||||
if not os.path.isfile(LIVE_STREAM_FRAME_PATH):
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
with open(LIVE_STREAM_FRAME_PATH, "rb") as f:
|
||||
jpeg = f.read()
|
||||
consecutive_fails = 0
|
||||
if not _is_valid_jpeg(jpeg):
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
yield (b"--frame\r\n"
|
||||
b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n")
|
||||
except FileNotFoundError:
|
||||
consecutive_fails += 1
|
||||
if consecutive_fails >= MAX_FAILS:
|
||||
return
|
||||
time.sleep(1.0)
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
except Exception:
|
||||
consecutive_fails += 1
|
||||
if consecutive_fails >= MAX_FAILS:
|
||||
return
|
||||
time.sleep(0.5)
|
||||
time.sleep(0.25)
|
||||
continue
|
||||
time.sleep(0.05)
|
||||
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
+13
-3
@@ -1002,6 +1002,18 @@ def draw_popups(img, popups, frame_idx):
|
||||
return alive
|
||||
|
||||
|
||||
def _write_live_frame_atomic(path, jpeg_bytes):
|
||||
"""Write JPEG atomically so dashboard readers never see a partial file."""
|
||||
dest = Path(path)
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(jpeg_bytes)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(tmp, dest)
|
||||
|
||||
|
||||
def connect_stream(source, warmup=WARMUP_FRAMES):
|
||||
attempts = 0
|
||||
while not shutdown_requested:
|
||||
@@ -1381,12 +1393,10 @@ def run():
|
||||
|
||||
if LIVE_STREAM_ENABLED and frame_idx % LIVE_STREAM_EVERY_N == 0:
|
||||
try:
|
||||
Path(LIVE_STREAM_FRAME_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
_, jpeg = cv2.imencode(
|
||||
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, LIVE_STREAM_QUALITY]
|
||||
)
|
||||
with open(LIVE_STREAM_FRAME_PATH, "wb") as f:
|
||||
f.write(jpeg.tobytes())
|
||||
_write_live_frame_atomic(LIVE_STREAM_FRAME_PATH, jpeg.tobytes())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -811,6 +811,9 @@ function updateToggleIcon() {
|
||||
let mainChart = null;
|
||||
let videoActive = false;
|
||||
let videoErrorTimer = null;
|
||||
let videoWatchdogTimer = null;
|
||||
let lastVideoFrameAt = 0;
|
||||
const VIDEO_STALL_MS = 8000;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadSummary();
|
||||
@@ -1083,6 +1086,32 @@ document.addEventListener('keydown', (e) => {
|
||||
|
||||
// --- Live Video Feed ---
|
||||
|
||||
function reconnectVideo() {
|
||||
if (!videoActive) return;
|
||||
const img = document.getElementById('live-video-img');
|
||||
if (!img) return;
|
||||
lastVideoFrameAt = Date.now();
|
||||
img.src = '/api/live-video?' + Date.now();
|
||||
}
|
||||
|
||||
function startVideoWatchdog() {
|
||||
stopVideoWatchdog();
|
||||
lastVideoFrameAt = Date.now();
|
||||
videoWatchdogTimer = setInterval(() => {
|
||||
if (!videoActive) return;
|
||||
if (Date.now() - lastVideoFrameAt > VIDEO_STALL_MS) {
|
||||
reconnectVideo();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopVideoWatchdog() {
|
||||
if (videoWatchdogTimer) {
|
||||
clearInterval(videoWatchdogTimer);
|
||||
videoWatchdogTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVideo() {
|
||||
const btn = document.getElementById('videoToggle');
|
||||
const img = document.getElementById('live-video-img');
|
||||
@@ -1093,14 +1122,16 @@ function toggleVideo() {
|
||||
|
||||
if (videoActive) {
|
||||
resetFilter();
|
||||
img.src = '/api/live-video?' + Date.now();
|
||||
img.style.display = 'block';
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
btn.textContent = 'STREAM ON';
|
||||
btn.classList.add('active');
|
||||
status.innerHTML = '<span style="width:6px;height:6px;border-radius:50%;background:var(--accent4);animation:blink 1.5s ease-in-out infinite;"></span> connecting...';
|
||||
status.className = 'video-status waiting';
|
||||
reconnectVideo();
|
||||
startVideoWatchdog();
|
||||
} else {
|
||||
stopVideoWatchdog();
|
||||
img.src = '';
|
||||
img.style.display = 'none';
|
||||
if (placeholder) placeholder.style.display = '';
|
||||
@@ -1113,6 +1144,7 @@ function toggleVideo() {
|
||||
}
|
||||
|
||||
function onVideoLoad() {
|
||||
lastVideoFrameAt = Date.now();
|
||||
const status = document.getElementById('video-status');
|
||||
status.innerHTML = '<span style="width:6px;height:6px;border-radius:50%;background:var(--accent4);box-shadow:0 0 6px var(--accent4);animation:blink 1.5s ease-in-out infinite;"></span> live';
|
||||
status.className = 'video-status connected';
|
||||
@@ -1124,10 +1156,7 @@ function onVideoError() {
|
||||
if (overlay) overlay.classList.add('visible');
|
||||
if (videoErrorTimer) clearTimeout(videoErrorTimer);
|
||||
videoErrorTimer = setTimeout(() => {
|
||||
if (videoActive) {
|
||||
const img = document.getElementById('live-video-img');
|
||||
img.src = '/api/live-video?' + Date.now();
|
||||
}
|
||||
reconnectVideo();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user