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

237 lines
7.6 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 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()
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()
_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)