// ==UserScript== // @name Twitch Mini Player + Latency // @namespace latency // @version 1.5 // @description Draggable Twitch mini-player + Twitch/Kick latency display // @match https://www.twitch.tv/* // @match https://kick.com/* // @grant none // @downloadURL none // ==/UserScript== (function() { const padding = 10, key = 'miniPlayerPos'; const platform = location.hostname.includes('kick.com') ? 'kick' : 'twitch'; let header = null, spinner = null; function getHeaderHeight() { const selectors = ['.top-nav','[data-a-target="top-nav"]','.top-nav__menu','header','.tw-header']; for (let s of selectors) { let e = document.querySelector(s); if (e) { let r = e.getBoundingClientRect(); if (r.height > 0) return r.bottom + padding; } } return 80 + padding; } function initializeMiniPlayer() { const player = document.querySelector('.persistent-player'); const miniPlayer = document.querySelector('.persistent-player__border--mini'); if (!miniPlayer || !player) return false; function setDefaultPosition() { let h = getHeaderHeight(); miniPlayer.style.left = padding + 'px'; miniPlayer.style.top = h + 'px'; miniPlayer.style.bottom = 'auto'; miniPlayer.style.right = 'auto'; } let saved = localStorage.getItem(key); if (saved) { try { let pos = JSON.parse(saved); let h = getHeaderHeight(); let maxL = window.innerWidth - miniPlayer.offsetWidth - padding; let maxT = window.innerHeight - miniPlayer.offsetHeight - padding; let safeTop = Math.max(h, Math.min(pos.top, maxT)); miniPlayer.style.left = Math.max(padding, Math.min(pos.left, maxL)) + 'px'; miniPlayer.style.top = safeTop + 'px'; } catch { setDefaultPosition(); } } else setDefaultPosition(); if (miniPlayer._dragInitialized) return true; let dragging = false, startX = 0, startY = 0, initLeft = 0, initTop = 0; miniPlayer.style.position = 'fixed'; miniPlayer.style.cursor = 'move'; miniPlayer.style.zIndex = '9999'; miniPlayer.style.margin = '0'; miniPlayer.addEventListener('mousedown', e => { if (e.target.closest('button') || e.target.closest('a')) return; dragging = true; startX = e.clientX; startY = e.clientY; let r = miniPlayer.getBoundingClientRect(); initLeft = r.left; initTop = r.top; miniPlayer.style.transition = 'none'; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); e.preventDefault(); }); function drag(e) { if (!dragging) return; let dx = e.clientX - startX, dy = e.clientY - startY; let newL = Math.max(padding, Math.min(initLeft + dx, window.innerWidth - miniPlayer.offsetWidth - padding)); let newT = Math.max(getHeaderHeight(), Math.min(initTop + dy, window.innerHeight - miniPlayer.offsetHeight - padding)); miniPlayer.style.left = newL + 'px'; miniPlayer.style.top = newT + 'px'; miniPlayer.style.bottom = miniPlayer.style.right = 'auto'; } function stopDrag() { if (!dragging) return; dragging = false; document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); let r = miniPlayer.getBoundingClientRect(); localStorage.setItem(key, JSON.stringify({ left: Math.round(r.left), top: Math.round(r.top) })); miniPlayer.style.transition = ''; } window.addEventListener('resize', () => { let r = miniPlayer.getBoundingClientRect(); let newL = Math.max(padding, Math.min(r.left, window.innerWidth - miniPlayer.offsetWidth - padding)); let newT = Math.max(getHeaderHeight(), Math.min(r.top, window.innerHeight - miniPlayer.offsetHeight - padding)); if (newL !== r.left || newT !== r.top) { miniPlayer.style.left = newL + 'px'; miniPlayer.style.top = newT + 'px'; } }); miniPlayer._dragInitialized = true; return true; } let attempts = 0, maxAttempts = 50; const tryInit = setInterval(() => { if (initializeMiniPlayer() || attempts >= maxAttempts) clearInterval(tryInit); attempts++; }, 100); new MutationObserver(() => { initializeMiniPlayer(); }).observe(document.body, { childList: true, subtree: true }); function styleHeader(el) { el.style.display = 'flex'; el.style.alignItems = 'center'; el.style.justifyContent = 'center'; el.style.color = '#fff'; el.style.fontWeight = '600'; el.style.fontSize = '15px'; el.style.cursor = 'pointer'; el.style.gap = '6px'; } function createRedDot() { const d = document.createElement('span'); d.id = 'latency-red-dot'; d.style.cssText = 'display:inline-block;width:8px;height:8px;border-radius:50%;background:#FF4B4B;'; return d; } async function readTwitchStats(timeoutMs = 1500) { const existing = document.querySelector('p[aria-label="Задержка до владельца канала"]'); if (existing) return existing.textContent.trim(); const toggle = () => { const e = { ctrlKey: true, altKey: true, shiftKey: true, code: 'KeyS', key: 'S', bubbles: true, cancelable: true }; document.dispatchEvent(new KeyboardEvent('keydown', e)); document.dispatchEvent(new KeyboardEvent('keyup', e)); }; try { toggle(); } catch {} const start = Date.now(); while (Date.now() - start < timeoutMs) { let p = document.querySelector('p[aria-label="Задержка до владельца канала"]'); if (p && p.textContent.trim().length) { let c = p.closest('table,.tw-stat,div'); if (c) c.style.display = 'none'; try { toggle(); } catch {} return p.textContent.trim(); } await new Promise(r => setTimeout(r, 150)); } try { toggle(); } catch {} return null; } function readKickLatency() { const v = document.querySelector('video'); if (!v || !v.buffered.length) return null; const lat = v.buffered.end(v.buffered.length - 1) - v.currentTime; return lat > 0 ? lat.toFixed(2) + 's' : '0.00s'; } async function getLatency() { if (platform === 'kick') return readKickLatency(); let val = await readTwitchStats(); if (!val) { const v = document.querySelector('video'); if (v && v.buffered.length) { let l = v.buffered.end(v.buffered.length - 1) - v.currentTime; return l > 0 ? l.toFixed(2) + 's' : '0.00s'; } return null; } const m = val.match(/([\d,.]+)\s*(сек|s|ms)?/i); if (m && m[1]) { let num = parseFloat(m[1].replace(',', '.')); if (m[2] && /ms/i.test(m[2])) num /= 1000; return num.toFixed(2) + 's'; } return val; } async function updateHeader() { if (!header) return; const lat = await getLatency(); if (!lat) return; header.innerHTML = ''; let dot = document.getElementById('latency-red-dot'); if (!dot) dot = createRedDot(); header.appendChild(dot); const s = document.createElement('span'); s.textContent = `Latency: ${lat}`; header.appendChild(s); } function createSpinner() { if (spinner) return spinner; spinner = document.createElement('div'); spinner.id = 'latency-spinner'; spinner.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;display:none;'; const v = document.querySelector('video'); if (v && v.parentElement) v.parentElement.appendChild(spinner); return spinner; } function reloadPlayer() { const v = document.querySelector('video'); if (!v) return; const sp = createSpinner(); sp.style.display = 'block'; const ct = v.currentTime; v.pause(); setTimeout(() => { try { v.currentTime = ct; v.play().catch(() => {}); } catch { location.reload(); } sp.style.display = 'none'; updateHeader(); }, 1200); } function findHeader() { let candidate = platform === 'twitch' ? document.querySelector('#chat-room-header-label') : Array.from(document.querySelectorAll('span.absolute')).find(e => e.textContent.trim() === 'Чат'); if (candidate && candidate !== header) { header = candidate; styleHeader(header); header.addEventListener('click', reloadPlayer); updateHeader(); } } const obs = new MutationObserver(findHeader); obs.observe(document.body, { childList: true, subtree: true }); setInterval(() => { if (header) updateHeader(); }, 2000); })();