From 47d67156b74aa6ec429189c0071f143641a42bc0 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Wed, 29 Jul 2026 14:54:50 +0700 Subject: [PATCH] Fix stream --- config.env.example | 10 +++-- recounting_dashboard.py | 85 ++++++++++++++++++++++++++------------- templates/recounting.html | 15 +++++++ 3 files changed, 78 insertions(+), 32 deletions(-) diff --git a/config.env.example b/config.env.example index a80ea93..94fd412 100644 --- a/config.env.example +++ b/config.env.example @@ -167,7 +167,9 @@ RECOUNTING_DASHBOARD_PORT=5002 LIVE_API_URL=http://localhost:5000 # Base URL of the recounting counter dashboard API (second node) RECOUNT_API_URL=http://localhost:5001 -# go2rtc REST API base URL for streaming MP4 files -GO2RTC_API_URL=http://localhost:1984 -# go2rtc stream name used for recounting preview -GO2RTC_STREAM_NAME=recount +# go2rtc base URL for consuming MJPEG stream (browser-facing) +GO2RTC_URL=http://localhost:1984 +# RTSP port used by ffmpeg for pushing stream to go2rtc +RTSP_PORT=8554 +# Stream name pushed to RTSP and consumed from go2rtc +RECOUNT_STREAM_NAME=stream_1 diff --git a/recounting_dashboard.py b/recounting_dashboard.py index df91b6c..4f76cbf 100644 --- a/recounting_dashboard.py +++ b/recounting_dashboard.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 """ Recounting dashboard — consumes live counter + recount APIs, -lists OUTPUT_DIR MP4 files, streams via go2rtc for preview. +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 +from flask import Flask, render_template, jsonify, request, Response, send_file from werkzeug.serving import WSGIRequestHandler from dotenv import load_dotenv @@ -20,11 +21,12 @@ 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", os.getenv("OUTPUT_DIR", "/opt/bytetrack-counter")) +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_API_URL = os.getenv("GO2RTC_API_URL", "http://localhost:1984") -GO2RTC_STREAM_NAME = os.getenv("GO2RTC_STREAM_NAME", "recount") +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") @@ -37,6 +39,10 @@ _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 + def _parse_filename(filename): m = _FILENAME_RE.match(filename) @@ -58,6 +64,18 @@ def _api_get(base_url, path, default=None): 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) @@ -97,6 +115,7 @@ def api_mp4_files(): @app.route("/api/start-recount", methods=["POST"]) def start_recount(): + global _streaming_mp4, _streaming_proc data = request.get_json(force=True) or {} mp4_path = data.get("path", "") if not mp4_path: @@ -104,20 +123,38 @@ def start_recount(): if not os.path.isfile(mp4_path): return jsonify({"success": False, "error": f"File not found: {mp4_path}"}), 404 - src = f"ffmpeg:{mp4_path}#video=h264#hardware" - try: - resp = requests.put( - f"{GO2RTC_API_URL}/api/streams", - params={"name": GO2RTC_STREAM_NAME, "src": src}, - timeout=5, - ) - if resp.status_code not in (200, 201): - return jsonify({"success": False, "error": f"go2rtc returned {resp.status_code}: {resp.text}"}), 502 - except Exception as e: - return jsonify({"success": False, "error": f"go2rtc unreachable: {e}"}), 502 + with _streaming_lock: + _stop_ffmpeg_locked() + 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 - stream_url = f"{GO2RTC_API_URL}/api/stream.mjpeg?src={GO2RTC_STREAM_NAME}" - return jsonify({"success": True, "stream_url": stream_url, "file": Path(mp4_path).name}) + return jsonify({ + "success": True, + "stream_url": f"{GO2RTC_URL}/api/stream.mjpeg?src={STREAM_NAME}", + "file": Path(mp4_path).name, + }) + + +@app.route("/api/stop-recount", methods=["POST"]) +def stop_recount(): + with _streaming_lock: + _stop_ffmpeg_locked() + return jsonify({"success": True}) + + +@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") @@ -163,20 +200,12 @@ def api_batch_result(): return jsonify({"success": False, "error": f"Batch #{batch_num} not found"}), 200 -@app.route("/api/stop-recount", methods=["POST"]) -def stop_recount(): - try: - requests.delete(f"{GO2RTC_API_URL}/api/streams", params={"name": GO2RTC_STREAM_NAME}, timeout=5) - except Exception: - pass - return jsonify({"success": True}) - - 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 API: {GO2RTC_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) diff --git a/templates/recounting.html b/templates/recounting.html index f620aff..11afd2b 100644 --- a/templates/recounting.html +++ b/templates/recounting.html @@ -412,6 +412,9 @@ + @@ -466,6 +469,7 @@ function selectFile(path, name) { selectedFile = null; batchResultLoaded = false; document.getElementById('btn-start').disabled = true; + document.getElementById('btn-download').disabled = true; document.getElementById('live-title').innerHTML = '◉ Live Counter'; document.getElementById('live-count').textContent = '--'; document.getElementById('live-label').textContent = 'Objects Counted'; @@ -478,6 +482,7 @@ function selectFile(path, name) { } selectedFile = path; document.getElementById('btn-start').disabled = false; + document.getElementById('btn-download').disabled = false; loadFiles(); loadBatchResult(path); } @@ -533,6 +538,16 @@ function formatSize(bytes) { return (bytes / 1073741824).toFixed(1) + ' GB'; } +function downloadFile() { + if (!selectedFile) return; + var a = document.createElement('a'); + a.href = '/api/download-mp4?file=' + encodeURIComponent(selectedFile); + a.download = selectedFile.split('/').pop(); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + // --- Streaming --- async function startRecount() {