Files
bytetrack-counter-dashboard/recounting_dashboard.py
T
2026-07-29 15:31:14 +07:00

248 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""
Recounting dashboard — consumes live counter + recount APIs,
lists OUTPUT_DIR MP4 files, streams MP4 via ffmpeg RTSP → go2rtc for preview.
"""
import json
import logging
import os
import re
import subprocess
import threading
import requests
from datetime import datetime
from pathlib import Path
from flask import Flask, render_template, jsonify, request, Response, send_file
from werkzeug.serving import WSGIRequestHandler
from dotenv import load_dotenv
load_dotenv()
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
app = Flask(__name__, template_folder="templates")
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me-in-production")
OUTPUT_DIR = os.getenv("OUTPUT_DIR", "/opt/bytetrack-counter")
LIVE_API_URL = os.getenv("LIVE_API_URL", "http://localhost:5000")
RECOUNT_API_URL = os.getenv("RECOUNT_API_URL", "http://localhost:5001")
GO2RTC_URL = os.getenv("GO2RTC_URL", "http://localhost:1984")
RTSP_PORT = os.getenv("RTSP_PORT", "8554")
STREAM_NAME = os.getenv("RECOUNT_STREAM_NAME", "stream_1")
SITE_NAME = os.getenv("SITE_NAME", "RECOUNT")
DASHBOARD_PORT = int(os.getenv("RECOUNTING_DASHBOARD_PORT", "5002"))
DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true"
_http_session = requests.Session()
_http_session.timeout = 3
_FILENAME_RE = re.compile(r"batch[_-](\d+)[_-](\d{8})[_-]\d{6}\.mp4", re.IGNORECASE)
_streaming_mp4 = None
_streaming_lock = threading.Lock()
_streaming_proc = None
_current_file = None
def _parse_filename(filename):
m = _FILENAME_RE.match(filename)
if m:
batch_num = int(m.group(1))
date_str = m.group(2)
date_formatted = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}"
return batch_num, date_formatted
return None, None
def _api_get(base_url, path, default=None):
try:
resp = _http_session.get(f"{base_url}{path}")
if resp.status_code == 200:
return resp.json()
except Exception:
pass
return default
def _stop_ffmpeg_locked():
global _streaming_proc, _streaming_mp4
if _streaming_proc:
try:
_streaming_proc.kill()
_streaming_proc.wait(timeout=3)
except Exception:
pass
_streaming_proc = None
_streaming_mp4 = None
@app.route("/")
def index():
return render_template("recounting.html", site_name=SITE_NAME, output_dir=OUTPUT_DIR)
@app.route("/api/live-progress")
def api_live_progress():
data = _api_get(LIVE_API_URL, "/api/current-batch")
if data and data.get("success"):
return jsonify(data)
return jsonify({"success": False, "count": 0, "batch_number": None, "error": "Live unreachable"}), 200
@app.route("/api/recount-progress")
def api_recount_progress():
data = _api_get(RECOUNT_API_URL, "/api/current-batch")
if data and data.get("success"):
return jsonify(data)
return jsonify({"success": False, "count": 0, "batch_number": None, "error": "Recount unreachable"}), 200
@app.route("/api/mp4-files")
def api_mp4_files():
files = []
output = Path(OUTPUT_DIR)
if output.exists():
for f in sorted(output.rglob("*.mp4"), key=lambda p: p.stat().st_mtime, reverse=True):
st = f.stat()
files.append({
"name": f.name,
"path": str(f),
"size": st.st_size,
"mtime": datetime.fromtimestamp(st.st_mtime).isoformat(),
})
return jsonify(files)
@app.route("/api/start-recount", methods=["POST"])
def start_recount():
global _streaming_mp4, _streaming_proc, _current_file
data = request.get_json(force=True) or {}
mp4_path = data.get("path", "")
if not mp4_path:
return jsonify({"success": False, "error": "Missing 'path'"}), 400
if not os.path.isfile(mp4_path):
return jsonify({"success": False, "error": f"File not found: {mp4_path}"}), 404
with _streaming_lock:
_stop_ffmpeg_locked()
try:
_http_session.post(f"{RECOUNT_API_URL}/api/reset", timeout=5)
except Exception:
pass
with _streaming_lock:
_current_file = mp4_path
cmd = [
"ffmpeg", "-re", "-i", mp4_path,
"-vf", "scale=640:480",
"-c:v", "libx264", "-preset", "veryfast",
"-c:a", "aac",
"-f", "rtsp", f"rtsp://localhost:{RTSP_PORT}/{STREAM_NAME}",
]
_streaming_proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL)
_streaming_mp4 = mp4_path
return jsonify({
"success": True,
"stream_url": f"{RECOUNT_API_URL}/api/live-video",
"file": Path(mp4_path).name,
"path": mp4_path,
})
@app.route("/api/stop-recount", methods=["POST"])
def stop_recount():
global _current_file
with _streaming_lock:
_stop_ffmpeg_locked()
_current_file = None
return jsonify({"success": True})
@app.route("/api/state")
def api_state():
with _streaming_lock:
streaming = _streaming_proc is not None and _streaming_proc.poll() is None
mp4 = _streaming_mp4
current_file = _current_file
result = {
"streaming": streaming,
}
if current_file:
result["file"] = current_file
result["file_name"] = Path(current_file).name
result["stream_url"] = f"{RECOUNT_API_URL}/api/live-video"
batch_num, date_str = _parse_filename(Path(current_file).name)
if batch_num is not None:
result["batch_number"] = batch_num
result["batch_date"] = date_str
return jsonify(result)
@app.route("/api/download-mp4")
def api_download_mp4():
fpath = request.args.get("file", "")
if not fpath or not os.path.isfile(fpath):
return jsonify({"success": False, "error": "File not found"}), 404
return send_file(fpath, as_attachment=True, download_name=Path(fpath).name)
@app.route("/api/batch-result")
def api_batch_result():
fpath = request.args.get("file", "")
filename = Path(fpath).name
batch_num, date_str = _parse_filename(filename)
if batch_num is None:
return jsonify({"success": False, "error": "Cannot parse batch number from filename"}), 200
def _match_in_batches(batches):
for b in batches:
if b.get("batch_number") == batch_num:
return b
return None
data = _api_get(LIVE_API_URL, f"/api/day-detail/{date_str}")
if data:
b = _match_in_batches(data.get("batches", []))
if b:
return jsonify({
"success": True,
"batch_number": batch_num,
"date": date_str,
"count": b["count"],
"start_time": b.get("start_time"),
"end_time": b.get("end_time"),
})
recent = _api_get(LIVE_API_URL, "/api/recent-batches?limit=100")
if recent:
for entry in recent if isinstance(recent, list) else recent.get("batches", []):
if isinstance(entry, dict) and entry.get("batch_number") == batch_num:
return jsonify({
"success": True,
"batch_number": batch_num,
"date": entry.get("date", date_str),
"count": entry["count"],
"start_time": entry.get("start_time"),
"end_time": entry.get("end_time"),
})
return jsonify({"success": False, "error": f"Batch #{batch_num} not found"}), 200
if __name__ == "__main__":
WSGIRequestHandler.protocol_version = "HTTP/1.1"
print(f"Recounting dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}")
print(f"Live API: {LIVE_API_URL}")
print(f"Recount API: {RECOUNT_API_URL}")
print(f"go2rtc: {GO2RTC_URL}")
print(f"RTSP: rtsp://localhost:{RTSP_PORT}/{STREAM_NAME}")
print(f"OUTPUT_DIR: {OUTPUT_DIR}")
app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)