// Global State let g_latest_frame_to_display = null; let g_is_movement_on_flag = false; let g_socket = null; let g_timeout_id = null; let g_idle_timeout = null; // NEW: Replace queue with a single variable and rendering state let g_latest_frame_data = null; let g_is_decoding = false; let g_last_render_time = 0; const FRAME_INTERVAL_MS = 125; // 125ms = 8 FPS (100ms = 10 FPS) const g_buffer_img = new Image(); const DOM = {}; // Cache for DOM elements // When the buffer image finishes decoding, push it to the visible image g_buffer_img.onload = function() { if (DOM.l0) DOM.l0.src = this.src; g_is_decoding = false; // Flag that we are ready for the next frame }; g_buffer_img.onload = function() { if (DOM.l0) DOM.l0.src = this.src; g_is_decoding = false; // Flag that we are ready for the next frame }; g_buffer_img.onerror = function() { console.warn("Frame decoding failed. Dropping corrupted frame."); g_is_decoding = false; }; // The core loop that enforces FPS and drops intermediate frames function process_frame_loop() { if (!g_is_movement_on_flag) return; const now = Date.now(); // 1. Throttle check (max 8 FPS) // 2. Producer-Consumer check (!g_is_decoding ensures we don't pile up on slow CPUs) if (!g_is_decoding && (now - g_last_render_time >= FRAME_INTERVAL_MS)) { if (g_latest_frame_data) { g_is_decoding = true; g_last_render_time = now; // Push to buffer (triggers onload when done) g_buffer_img.src = g_latest_frame_data; // Consume the frame. If the websocket receives 3 frames while the // CPU is busy decoding this one, the next loop will only grab the newest. g_latest_frame_data = null; } } // requestAnimationFrame is kinder to old CPUs than setInterval. // It pauses automatically if the tab is minimized or hidden. requestAnimationFrame(process_frame_loop); } // Lightweight wrapper to replace jQuery $.get async function fetchText(url, callback) { try { const response = await fetch(url); if (response.ok) { const text = await response.text(); callback(text); } } catch (err) { console.error("Error fetching " + url, err); } } function update_collages() { try { const ts = Date.now(); if (DOM.v1) { DOM.v1.src = "/last.mp4?ts=" + ts; DOM.v1.load(); } if (DOM.v2) { DOM.v2.src = "/last2.mp4?ts=" + ts; DOM.v2.load(); } } catch (error) { console.log("Error in update_collages(): ", error); } } function update_time() { const date_obj = new Date(); const hrs = String(date_obj.getHours() % 12 || 12).padStart(2, '0'); const mins = String(date_obj.getMinutes()).padStart(2, '0'); if (DOM.clock) DOM.clock.innerText = hrs + ":" + mins; } function get_ws_url() { const l = window.location; return ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + l.pathname; } function set_mode_idle() { if (g_idle_timeout !== null) { clearTimeout(g_idle_timeout); } g_idle_timeout = setTimeout(function() { g_is_movement_on_flag = false; document.body.classList.remove('movement-active'); g_latest_frame_data = null; g_is_decoding = false; g_idle_timeout = null; update_collages(); }, 5000); } function connect_websocket() { if (typeof io === 'undefined') { setTimeout(connect_websocket, 50); return; } const ws_uri = get_ws_url(); try { if (g_socket) { g_socket.disconnect(); g_socket = null; } } catch (error) {} g_socket = io.connect(ws_uri); g_socket.on('connect', function () { update_collages(); }); g_socket.on('refresh', function (refresh_cmd) { g_buffer_img.src = refresh_cmd.data; update_collages(); }); g_socket.on('latest_live_frame', function(llf_status) { g_latest_frame_to_display = llf_status.data; g_timeout_id = null; g_buffer_img.src = llf_status.data; }); g_socket.on('movement', function (movement_status) { if (g_idle_timeout !== null) { clearTimeout(g_idle_timeout); g_idle_timeout = null; } // Just overwrite the variable. Intermediate frames are implicitly dropped. if (movement_status && movement_status.data) { g_latest_frame_data = movement_status.data; } if (!g_is_movement_on_flag) { g_is_movement_on_flag = true; document.body.classList.add('movement-active'); process_frame_loop(); } }); g_socket.on('idle', set_mode_idle); g_socket.on('movement_finished', set_mode_idle); g_socket.on('collage', function () { update_collages(); }); } document.addEventListener('DOMContentLoaded', function () { // Populate the DOM cache once the HTML has loaded DOM.l0 = document.getElementById('l0'); DOM.v1 = document.getElementById('v1'); DOM.v2 = document.getElementById('v2'); DOM.lmfts = document.getElementById('lmfts'); DOM.clock = document.getElementById('clock'); DOM.ip = document.getElementById('ip'); DOM.a_live = document.getElementById('a_live'); update_time(); connect_websocket(); fetchText("/lmf", function(data) { g_buffer_img.src = data; }); fetchText("/ip", function (data) { if (DOM.ip) DOM.ip.innerText = data; }); if (DOM.a_live) { DOM.a_live.addEventListener('click', function () { window.location.replace("/live"); }); } setInterval(update_time, 15000); }); document.addEventListener('visibilitychange', function() { if (!document.hidden) { location.reload(); } });