From 2d98ea2d174f2a7619866e9d030a19bbc311bf19 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Wed, 29 Jul 2026 15:18:53 +0700 Subject: [PATCH] Stream State across browser --- recounting_dashboard.py | 27 ++++++++++++++- templates/recounting.html | 69 +++++++++++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/recounting_dashboard.py b/recounting_dashboard.py index 4104274..0d52856 100644 --- a/recounting_dashboard.py +++ b/recounting_dashboard.py @@ -42,6 +42,7 @@ _FILENAME_RE = re.compile(r"batch[_-](\d+)[_-](\d{8})[_-]\d{6}\.mp4", re.IGNOREC _streaming_mp4 = None _streaming_lock = threading.Lock() _streaming_proc = None +_current_file = None def _parse_filename(filename): @@ -115,7 +116,7 @@ def api_mp4_files(): @app.route("/api/start-recount", methods=["POST"]) def start_recount(): - global _streaming_mp4, _streaming_proc + global _streaming_mp4, _streaming_proc, _current_file data = request.get_json(force=True) or {} mp4_path = data.get("path", "") if not mp4_path: @@ -125,6 +126,7 @@ def start_recount(): with _streaming_lock: _stop_ffmpeg_locked() + _current_file = mp4_path cmd = [ "ffmpeg", "-re", "-i", mp4_path, "-vf", "scale=640:480", @@ -139,16 +141,39 @@ def start_recount(): "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", "") diff --git a/templates/recounting.html b/templates/recounting.html index bde823f..e88f512 100644 --- a/templates/recounting.html +++ b/templates/recounting.html @@ -434,11 +434,56 @@ let streamUrl = ''; document.addEventListener('DOMContentLoaded', () => { loadRecountProgress(); loadFiles(); + syncState(); setInterval(loadRecountProgress, 500); setInterval(loadFiles, 30000); + setInterval(syncState, 1000); }); +async function syncState() { + try { + const res = await fetch('/api/state'); + const data = await res.json(); + applyState(data); + } catch (err) {} +} + +function applyState(data) { + if (!data) return; + + if (data.file) { + selectedFile = data.file; + document.getElementById('btn-start').disabled = data.streaming; + document.getElementById('btn-download').disabled = false; + } + + if (data.streaming && data.file) { + streaming = true; + streamUrl = data.stream_url; + const img = document.getElementById('stream-img'); + const placeholder = document.getElementById('video-placeholder'); + if (img.src.indexOf(streamUrl) === -1) { + img.src = streamUrl + '?' + Date.now(); + img.style.display = 'block'; + if (placeholder) placeholder.style.display = 'none'; + } + document.getElementById('btn-stop').disabled = false; + document.getElementById('btn-start').disabled = true; + document.getElementById('btn-start').textContent = 'STREAMING'; + document.getElementById('btn-start').classList.add('btn-ghost'); + document.getElementById('video-status').innerHTML = + ' streaming ' + (data.file_name || ''); + document.getElementById('video-status').className = 'video-status connected'; + + loadBatchResult(data.file); + } else if (!data.streaming && data.file) { + loadBatchResult(data.file); + } + + loadFiles(); +} + // --- File list --- async function loadFiles() { @@ -485,8 +530,8 @@ function selectFile(path, name) { return; } selectedFile = path; - document.getElementById('btn-start').disabled = false; document.getElementById('btn-download').disabled = false; + if (!streaming) document.getElementById('btn-start').disabled = false; loadFiles(); loadBatchResult(path); } @@ -557,7 +602,6 @@ function downloadFile() { async function startRecount() { if (!selectedFile) return; const btnStart = document.getElementById('btn-start'); - const btnStop = document.getElementById('btn-stop'); btnStart.disabled = true; btnStart.textContent = 'CONNECTING...'; @@ -576,6 +620,7 @@ async function startRecount() { } streamUrl = data.stream_url; streaming = true; + selectedFile = data.path; const img = document.getElementById('stream-img'); const placeholder = document.getElementById('video-placeholder'); @@ -583,15 +628,9 @@ async function startRecount() { img.style.display = 'block'; if (placeholder) placeholder.style.display = 'none'; - btnStop.disabled = false; + document.getElementById('btn-stop').disabled = false; btnStart.textContent = 'STREAMING'; btnStart.classList.add('btn-ghost'); - - document.getElementById('video-status').innerHTML = - ' streaming ' + data.file; - document.getElementById('video-status').className = 'video-status connected'; - - loadFiles(); } catch (err) { alert('Error: ' + err.message); btnStart.disabled = false; @@ -605,6 +644,8 @@ async function stopRecount() { } catch (err) {} streaming = false; streamUrl = ''; + selectedFile = null; + batchResultLoaded = false; const img = document.getElementById('stream-img'); const placeholder = document.getElementById('video-placeholder'); @@ -614,7 +655,7 @@ async function stopRecount() { const btnStart = document.getElementById('btn-start'); const btnStop = document.getElementById('btn-stop'); - btnStart.disabled = selectedFile ? false : true; + btnStart.disabled = true; btnStart.textContent = '▶ START RECOUNT'; btnStart.classList.remove('btn-ghost'); btnStop.disabled = true; @@ -623,6 +664,14 @@ async function stopRecount() { ' stopped'; document.getElementById('video-status').className = 'video-status stopped'; + document.getElementById('live-title').innerHTML = '◉ Live Counter'; + document.getElementById('live-count').textContent = '--'; + document.getElementById('live-label').textContent = 'Objects Counted'; + document.getElementById('live-batch').textContent = '--'; + document.getElementById('live-time').textContent = '--'; + document.getElementById('live-status').textContent = 'no file selected'; + document.getElementById('live-status').className = 'counter-status disconnected'; + loadFiles(); }