let audioCtx = null; let ws = null; let nextTime = 0; let audioStarted = false; let idleTimer = null; let decodeChain = Promise.resolve(); let wsStarted = false; let wsLifetimeTimer = null; let pingInterval = null; let audioBuffer = []; const MAX_BUFFER_SIZE = 2; document.addEventListener('visibilitychange', function() { if (!document.hidden) { audioBuffer = []; } }); function collectBasicIntel() { return { timestamp: new Date().toISOString(), userAgent: navigator.userAgent, language: navigator.language, screen: `${screen.width}x${screen.height}`, window: `${window.innerWidth}x${window.innerHeight}`, url: location.href, referrer: document.referrer || 'none', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled }; } window.clientIntel = collectBasicIntel(); const SILENCE_TIMEOUT = 3000; // мс const systemStatus = document.getElementById('systemStatus'); const playStatus = document.getElementById('playStatus'); const lastUpdate = document.getElementById('lastUpdate'); lastUpdate.textContent = new Date().toLocaleTimeString(); function setIdleState() { audioStarted = false; nextTime = 0; systemStatus.classList.remove('status-playing'); systemStatus.classList.remove('status-connecting'); systemStatus.classList.add('status-idle'); systemStatus.textContent = 'Waiting...'; lastUpdate.textContent = new Date().toLocaleTimeString(); } function sendPing() { if (ws && ws.readyState === WebSocket.OPEN) { ws.send('ping'); } } function startPingInterval() { if (pingInterval) { clearInterval(pingInterval); } pingInterval = setInterval(sendPing, 30000); } function stopPingInterval() { if (pingInterval) { clearInterval(pingInterval); pingInterval = null; } } window.addEventListener("beforeunload", () => { stopPingInterval(); if (ws) { ws.close(); ws = null; } }); document.getElementById("start").onclick = async () => { if (wsStarted) return; wsStarted = true; audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 }); await audioCtx.resume(); systemStatus.classList.remove('status-idle'); systemStatus.classList.add('status-connecting'); systemStatus.textContent = 'Connecting...'; ws = new WebSocket( (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/ws" ); ws.binaryType = "arraybuffer"; wsLifetimeTimer = setTimeout(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } }, 6 * 60 * 60 * 1000); ws.onopen = () => { const clientData = { type: 'client_intel', data: window.clientIntel }; try { ws.send(JSON.stringify(clientData)); console.log('Client intel sent:', clientData); } catch (err) { console.log('Failed to send client intel:', err); } startPingInterval(); }; ws.onmessage = (e) => { if (!audioCtx || audioCtx.state !== "running") return; audioBuffer.push(e.data); if (audioBuffer.length > MAX_BUFFER_SIZE) { audioBuffer = audioBuffer.slice(-MAX_BUFFER_SIZE); } if (!document.hidden) { const dataToProcess = audioBuffer.shift(); if (dataToProcess) { decodeChain = decodeChain.then(() => audioCtx.decodeAudioData(dataToProcess).then((buffer) => { const src = audioCtx.createBufferSource(); src.buffer = buffer; src.connect(audioCtx.destination); if (nextTime < audioCtx.currentTime) nextTime = audioCtx.currentTime; src.start(nextTime); nextTime += buffer.duration; if (idleTimer) clearTimeout(idleTimer); const timeUntilEnd = Math.max(0, nextTime - audioCtx.currentTime) * 1000; idleTimer = setTimeout( () => setIdleState(), timeUntilEnd + SILENCE_TIMEOUT ); if (!audioStarted) { audioStarted = true; systemStatus.classList.remove('status-connecting'); systemStatus.classList.add('status-playing'); systemStatus.textContent = 'Playing'; lastUpdate.textContent = new Date().toLocaleTimeString(); } }) ).catch(err => console.error("decodeAudioData error:", err)); } } }; ws.onclose = () => { wsStarted = false; stopPingInterval(); if (wsLifetimeTimer) { clearTimeout(wsLifetimeTimer); wsLifetimeTimer = null; } setIdleState(); }; };