Fix stream
This commit is contained in:
1 parent
e84161901f
commit
47d67156b7
3 files changed
+78
-32
No files matched your search
+6
-4
@@ -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
@@ -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)
|
||||||
@@ -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>
|
||||||
■ STOP
|
■ 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 = '◉ Live Counter';
|
document.getElementById('live-title').innerHTML = '◉ 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() {
|
||||||
|
|||||||
Reference in new issue
Block a user