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 |
|
| 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` |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
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")
|
DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
|
||||||
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true"
|
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")
|
@app.route("/api/live-video")
|
||||||
def 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():
|
def generate():
|
||||||
consecutive_fails = 0
|
|
||||||
MAX_FAILS = 30
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
if not os.path.isfile(LIVE_STREAM_FRAME_PATH):
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
with open(LIVE_STREAM_FRAME_PATH, "rb") as f:
|
with open(LIVE_STREAM_FRAME_PATH, "rb") as f:
|
||||||
jpeg = f.read()
|
jpeg = f.read()
|
||||||
consecutive_fails = 0
|
if not _is_valid_jpeg(jpeg):
|
||||||
|
time.sleep(0.05)
|
||||||
|
continue
|
||||||
yield (b"--frame\r\n"
|
yield (b"--frame\r\n"
|
||||||
b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n")
|
b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
consecutive_fails += 1
|
time.sleep(0.5)
|
||||||
if consecutive_fails >= MAX_FAILS:
|
|
||||||
return
|
|
||||||
time.sleep(1.0)
|
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
consecutive_fails += 1
|
time.sleep(0.25)
|
||||||
if consecutive_fails >= MAX_FAILS:
|
|
||||||
return
|
|
||||||
time.sleep(0.5)
|
|
||||||
continue
|
continue
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
|
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
|
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):
|
def connect_stream(source, warmup=WARMUP_FRAMES):
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while not shutdown_requested:
|
while not shutdown_requested:
|
||||||
@@ -1381,12 +1393,10 @@ def run():
|
|||||||
|
|
||||||
if LIVE_STREAM_ENABLED and frame_idx % LIVE_STREAM_EVERY_N == 0:
|
if LIVE_STREAM_ENABLED and frame_idx % LIVE_STREAM_EVERY_N == 0:
|
||||||
try:
|
try:
|
||||||
Path(LIVE_STREAM_FRAME_PATH).parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
_, jpeg = cv2.imencode(
|
_, jpeg = cv2.imencode(
|
||||||
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, LIVE_STREAM_QUALITY]
|
".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, LIVE_STREAM_QUALITY]
|
||||||
)
|
)
|
||||||
with open(LIVE_STREAM_FRAME_PATH, "wb") as f:
|
_write_live_frame_atomic(LIVE_STREAM_FRAME_PATH, jpeg.tobytes())
|
||||||
f.write(jpeg.tobytes())
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -811,6 +811,9 @@ function updateToggleIcon() {
|
|||||||
let mainChart = null;
|
let mainChart = null;
|
||||||
let videoActive = false;
|
let videoActive = false;
|
||||||
let videoErrorTimer = null;
|
let videoErrorTimer = null;
|
||||||
|
let videoWatchdogTimer = null;
|
||||||
|
let lastVideoFrameAt = 0;
|
||||||
|
const VIDEO_STALL_MS = 8000;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadSummary();
|
loadSummary();
|
||||||
@@ -1083,6 +1086,32 @@ document.addEventListener('keydown', (e) => {
|
|||||||
|
|
||||||
// --- Live Video Feed ---
|
// --- 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() {
|
function toggleVideo() {
|
||||||
const btn = document.getElementById('videoToggle');
|
const btn = document.getElementById('videoToggle');
|
||||||
const img = document.getElementById('live-video-img');
|
const img = document.getElementById('live-video-img');
|
||||||
@@ -1093,14 +1122,16 @@ function toggleVideo() {
|
|||||||
|
|
||||||
if (videoActive) {
|
if (videoActive) {
|
||||||
resetFilter();
|
resetFilter();
|
||||||
img.src = '/api/live-video?' + Date.now();
|
|
||||||
img.style.display = 'block';
|
img.style.display = 'block';
|
||||||
if (placeholder) placeholder.style.display = 'none';
|
if (placeholder) placeholder.style.display = 'none';
|
||||||
btn.textContent = 'STREAM ON';
|
btn.textContent = 'STREAM ON';
|
||||||
btn.classList.add('active');
|
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.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';
|
status.className = 'video-status waiting';
|
||||||
|
reconnectVideo();
|
||||||
|
startVideoWatchdog();
|
||||||
} else {
|
} else {
|
||||||
|
stopVideoWatchdog();
|
||||||
img.src = '';
|
img.src = '';
|
||||||
img.style.display = 'none';
|
img.style.display = 'none';
|
||||||
if (placeholder) placeholder.style.display = '';
|
if (placeholder) placeholder.style.display = '';
|
||||||
@@ -1113,6 +1144,7 @@ function toggleVideo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onVideoLoad() {
|
function onVideoLoad() {
|
||||||
|
lastVideoFrameAt = Date.now();
|
||||||
const status = document.getElementById('video-status');
|
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.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';
|
status.className = 'video-status connected';
|
||||||
@@ -1124,10 +1156,7 @@ function onVideoError() {
|
|||||||
if (overlay) overlay.classList.add('visible');
|
if (overlay) overlay.classList.add('visible');
|
||||||
if (videoErrorTimer) clearTimeout(videoErrorTimer);
|
if (videoErrorTimer) clearTimeout(videoErrorTimer);
|
||||||
videoErrorTimer = setTimeout(() => {
|
videoErrorTimer = setTimeout(() => {
|
||||||
if (videoActive) {
|
reconnectVideo();
|
||||||
const img = document.getElementById('live-video-img');
|
|
||||||
img.src = '/api/live-video?' + Date.now();
|
|
||||||
}
|
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user