Fix stream

This commit is contained in:
proitlab committed 2026-07-29 14:54:50 +07:00
1 parent e84161901f
commit 47d67156b7
3 files changed
+78 -32

No files matched your search

+6 -4
View File
@@ -167,7 +167,9 @@ RECOUNTING_DASHBOARD_PORT=5002
LIVE_API_URL=http://localhost:5000 LIVE_API_URL=http://localhost:5000
# Base URL of the recounting counter dashboard API (second node) # Base URL of the recounting counter dashboard API (second node)
RECOUNT_API_URL=http://localhost:5001 RECOUNT_API_URL=http://localhost:5001
# go2rtc REST API base URL for streaming MP4 files # go2rtc base URL for consuming MJPEG stream (browser-facing)
GO2RTC_API_URL=http://localhost:1984 GO2RTC_URL=http://localhost:1984
# go2rtc stream name used for recounting preview # RTSP port used by ffmpeg for pushing stream to go2rtc
GO2RTC_STREAM_NAME=recount RTSP_PORT=8554
# Stream name pushed to RTSP and consumed from go2rtc
RECOUNT_STREAM_NAME=stream_1
+57 -28
View File
@@ -1,17 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Recounting dashboard — consumes live counter + recount APIs, 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 json
import os import os
import re import re
import subprocess import subprocess
import threading
import requests import requests
from datetime import datetime from datetime import datetime
from pathlib import Path 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 werkzeug.serving import WSGIRequestHandler
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -20,11 +21,12 @@ load_dotenv()
app = Flask(__name__, template_folder="templates") app = Flask(__name__, template_folder="templates")
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me-in-production") 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") LIVE_API_URL = os.getenv("LIVE_API_URL", "http://localhost:5000")
RECOUNT_API_URL = os.getenv("RECOUNT_API_URL", "http://localhost:5001") RECOUNT_API_URL = os.getenv("RECOUNT_API_URL", "http://localhost:5001")
GO2RTC_API_URL = os.getenv("GO2RTC_API_URL", "http://localhost:1984") GO2RTC_URL = os.getenv("GO2RTC_URL", "http://localhost:1984")
GO2RTC_STREAM_NAME = os.getenv("GO2RTC_STREAM_NAME", "recount") RTSP_PORT = os.getenv("RTSP_PORT", "8554")
STREAM_NAME = os.getenv("RECOUNT_STREAM_NAME", "stream_1")
SITE_NAME = os.getenv("SITE_NAME", "RECOUNT") 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) _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): def _parse_filename(filename):
m = _FILENAME_RE.match(filename) m = _FILENAME_RE.match(filename)
@@ -58,6 +64,18 @@ def _api_get(base_url, path, default=None):
return default 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("/") @app.route("/")
def index(): def index():
return render_template("recounting.html", site_name=SITE_NAME, output_dir=OUTPUT_DIR) 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"]) @app.route("/api/start-recount", methods=["POST"])
def start_recount(): def start_recount():
global _streaming_mp4, _streaming_proc
data = request.get_json(force=True) or {} data = request.get_json(force=True) or {}
mp4_path = data.get("path", "") mp4_path = data.get("path", "")
if not mp4_path: if not mp4_path:
@@ -104,20 +123,38 @@ def start_recount():
if not os.path.isfile(mp4_path): if not os.path.isfile(mp4_path):
return jsonify({"success": False, "error": f"File not found: {mp4_path}"}), 404 return jsonify({"success": False, "error": f"File not found: {mp4_path}"}), 404
src = f"ffmpeg:{mp4_path}#video=h264#hardware" with _streaming_lock:
try: _stop_ffmpeg_locked()
resp = requests.put( cmd = [
f"{GO2RTC_API_URL}/api/streams", "ffmpeg", "-re", "-i", mp4_path,
params={"name": GO2RTC_STREAM_NAME, "src": src}, "-vf", "scale=640:480",
timeout=5, "-c:v", "libx264", "-preset", "veryfast",
) "-c:a", "aac",
if resp.status_code not in (200, 201): "-f", "rtsp", f"rtsp://localhost:{RTSP_PORT}/{STREAM_NAME}",
return jsonify({"success": False, "error": f"go2rtc returned {resp.status_code}: {resp.text}"}), 502 ]
except Exception as e: _streaming_proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL)
return jsonify({"success": False, "error": f"go2rtc unreachable: {e}"}), 502 _streaming_mp4 = mp4_path
stream_url = f"{GO2RTC_API_URL}/api/stream.mjpeg?src={GO2RTC_STREAM_NAME}" return jsonify({
return jsonify({"success": True, "stream_url": stream_url, "file": Path(mp4_path).name}) "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") @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 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__": if __name__ == "__main__":
WSGIRequestHandler.protocol_version = "HTTP/1.1" WSGIRequestHandler.protocol_version = "HTTP/1.1"
print(f"Recounting dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}") print(f"Recounting dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}")
print(f"Live API: {LIVE_API_URL}") print(f"Live API: {LIVE_API_URL}")
print(f"Recount API: {RECOUNT_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}") print(f"OUTPUT_DIR: {OUTPUT_DIR}")
app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG) app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)
+15
View File
@@ -412,6 +412,9 @@
<button class="btn btn-danger" id="btn-stop" onclick="stopRecount()" disabled> <button class="btn btn-danger" id="btn-stop" onclick="stopRecount()" disabled>
&#9632; STOP &#9632; STOP
</button> </button>
<button class="btn btn-ghost" id="btn-download" onclick="downloadFile()" disabled>
<i class="fas fa-download"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -466,6 +469,7 @@ function selectFile(path, name) {
selectedFile = null; selectedFile = null;
batchResultLoaded = false; batchResultLoaded = false;
document.getElementById('btn-start').disabled = true; document.getElementById('btn-start').disabled = true;
document.getElementById('btn-download').disabled = true;
document.getElementById('live-title').innerHTML = '&#9673; Live Counter'; document.getElementById('live-title').innerHTML = '&#9673; Live Counter';
document.getElementById('live-count').textContent = '--'; document.getElementById('live-count').textContent = '--';
document.getElementById('live-label').textContent = 'Objects Counted'; document.getElementById('live-label').textContent = 'Objects Counted';
@@ -478,6 +482,7 @@ function selectFile(path, name) {
} }
selectedFile = path; selectedFile = path;
document.getElementById('btn-start').disabled = false; document.getElementById('btn-start').disabled = false;
document.getElementById('btn-download').disabled = false;
loadFiles(); loadFiles();
loadBatchResult(path); loadBatchResult(path);
} }
@@ -533,6 +538,16 @@ function formatSize(bytes) {
return (bytes / 1073741824).toFixed(1) + ' GB'; 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 --- // --- Streaming ---
async function startRecount() { async function startRecount() {