// app.js (оркестратор) import { fetchHealth, fetchHistory } from "./data.js"; import { state, addSignals, resetBufferTracking, updatePeakHolder, updateSignalPeakHolder, setPeakHolderListener } from "./state.js"; import { initDOM, setServerTime, setStatus, setMeta, setBars, setSignal, setAudioIndicator, DOM } from "./ui.js"; import { initAccordion } from "./accordion.js"; import { drawChart } from "./chart.js"; import { initResize } from "./layout.js"; import { initCamera } from "./camera.js"; import { updateAllChannels } from './ui.js'; import { audioEngine } from "./audio.js"; let lastChart: number = 0; let connectionLost: boolean = false; let consecutiveErrors: number = 0; const MAX_ERRORS: number = 3; let pollTimer: number | null = null; let lastSignalLevel: number = 0; // ================= INIT ================= window.addEventListener("DOMContentLoaded", () => { initDOM(); initAccordion(); initResize(); initCamera(); // Инициализация звука при первом клике document.addEventListener('click', () => { audioEngine.updateLevel(0); }, { once: true }); setPeakHolderListener(() => { if (!connectionLost) { setBars(state.peakValue); setSignal(state.peakSignalLevel); const signalLevel = state.peakSignalLevel; if (signalLevel !== lastSignalLevel) { lastSignalLevel = signalLevel; audioEngine.updateLevel(signalLevel); // Обновляем индикатор звука const isActive = signalLevel > 0; setAudioIndicator(isActive, signalLevel); } } }); }); document.addEventListener('DOMContentLoaded', () => { // Обработчик для кнопки закрытия камеры const closeBtn = document.getElementById('camCloseBtn'); const toggleBtn = document.getElementById('camToggle'); if (closeBtn) { closeBtn.addEventListener('click', () => { if (toggleBtn) { toggleBtn.click(); // Эмулируем нажатие кнопки переключения } }); } }); document.addEventListener("keydown", (e: KeyboardEvent) => { const leftPanel = document.getElementById("leftPanel"); const rightPanel = document.getElementById("rightPanel"); switch(e.key.toLowerCase()) { case 'c': const camBtn = document.getElementById('camToggle'); if (camBtn) camBtn.click(); break; case 'f': if (leftPanel) { leftPanel.style.width = "100%"; if (rightPanel) rightPanel.style.display = "none"; } break; case 'escape': if (rightPanel) rightPanel.style.display = "flex"; if (leftPanel && rightPanel) { const total = window.innerWidth; const leftWidth = total * 0.6; leftPanel.style.flex = `0 0 ${leftWidth}px`; rightPanel.style.flex = `0 0 ${total - leftWidth}px`; } break; case 'm': // Toggle звука if (lastSignalLevel > 0) { // Временно отключаем audioEngine.updateLevel(0); setAudioIndicator(false); console.log('[Audio] Звук отключён (M)'); // Сохраняем уровень для восстановления window._mutedLevel = lastSignalLevel; lastSignalLevel = 0; } else { // Восстанавливаем уровень const level = window._mutedLevel || state.peakSignalLevel; if (level > 0) { audioEngine.updateLevel(level); lastSignalLevel = level; setAudioIndicator(true, level); console.log('[Audio] Звук включён (M)'); } } break; } }); // ================= UPDATE ================= async function update(): Promise { try { const [health, history] = await Promise.all([ fetchHealth(), fetchHistory() ]); consecutiveErrors = 0; if (health.server_time) { setServerTime(health.server_time); } if (connectionLost) { console.log("[app] Соединение восстановлено"); } const isOnline = health.pipe_alive && health.has_data; if (isOnline) { setStatus("На связи"); connectionLost = false; } else { setStatus("Нет связи", true); if (!connectionLost) resetBufferTracking(); connectionLost = true; if (lastSignalLevel > 0) { lastSignalLevel = 0; audioEngine.updateLevel(0); setAudioIndicator(false); } } const arr = Array.isArray(history.bytes) ? history.bytes : []; const raw = arr[arr.length - 1] ?? 0; if (arr.length > 0 && isOnline) { const bufferSize = health.stats?.filled ?? 0; addSignals(arr, bufferSize); updateAllChannels(arr); } const value = raw & 0x3F; const level = (raw >> 6) & 0x3; const percent = (value / 63) * 100; if (isOnline) { const displayValue = updatePeakHolder(percent); setBars(displayValue); const displaySignalLevel = updateSignalPeakHolder(level); setSignal(displaySignalLevel); const currentSignalLevel = state.peakSignalLevel; if (currentSignalLevel !== lastSignalLevel) { lastSignalLevel = currentSignalLevel; audioEngine.updateLevel(currentSignalLevel); setAudioIndicator(currentSignalLevel > 0, currentSignalLevel); } } else { setBars(0, true); setSignal(0, true); if (lastSignalLevel > 0) { lastSignalLevel = 0; audioEngine.updateLevel(0); setAudioIndicator(false); } } if (DOM.lastByte) { DOM.lastByte.textContent = isOnline ? value.toString() : "--"; } const uptime = Math.round(health.uptime_sec || 0); const filled = health.stats?.filled ?? 0; const Bps = Math.round(health.stats?.bytes_per_sec ?? 0); setMeta(`Работа: ${uptime}s | Буфер: ${filled} | Приём: ${Bps} B/s`); if (DOM.history) { DOM.history.innerHTML = arr .map((b, i) => `[${i}] ${(b & 0xFF).toString(2).padStart(8, "0")} = ${b & 0xFF}`) .join("
"); } const now = performance.now(); if (now - lastChart > 100 && DOM.canvas) { drawChart(DOM.canvas, state.signalHistory); lastChart = now; } } catch (e) { consecutiveErrors++; if (consecutiveErrors === 1) console.warn("[app] Сервер недоступен, ждём..."); setStatus("СЕРВЕР НЕДОСТУПЕН", true); setServerTime("--:--:--"); setBars(0, true); setSignal(0, true); resetBufferTracking(); if (lastSignalLevel > 0) { lastSignalLevel = 0; audioEngine.updateLevel(0); setAudioIndicator(false); } if (DOM.lastByte) DOM.lastByte.textContent = "--"; if (consecutiveErrors >= MAX_ERRORS && pollTimer) { clearInterval(pollTimer); pollTimer = window.setInterval(update, 2000); console.log("[app] Замедлили опрос до 2с"); } } } function startPolling(): void { if (pollTimer) clearInterval(pollTimer); if (consecutiveErrors > 0) update(); pollTimer = window.setInterval(update, 500); } window.addEventListener("focus", () => { if (consecutiveErrors >= MAX_ERRORS) { console.log("[app] Окно в фокусе, ускоряем опрос"); consecutiveErrors = 0; startPolling(); } }); window.addEventListener("beforeunload", () => { audioEngine.dispose(); }); declare global { interface Window { _mutedLevel?: number; } } startPolling(); update();