// ==UserScript== // @name Twitch & Kick Latency // @namespace latency // @version 1.3 // @description Displays the Twitch and Kick play latency // @author frz // @icon https://www.allkeyshop.com/blog/wp-content/uploads/Twitch-vs-Kick_featured.png // @match https://www.twitch.tv/* // @match https://kick.com/* // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; let header = null; const platform = location.hostname.includes('kick.com') ? 'kick' : 'twitch'; let spinner = null; 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 dot = document.createElement('span'); dot.id = 'latency-red-dot'; dot.style.cssText = 'display:inline-block;width:8px;height:8px;border-radius:50%;background:#FF4B4B;'; return dot; } async function readTwitchStats(timeoutMs = 1500) { const existing = document.querySelector('p[aria-label="Задержка до владельца канала"]'); if (existing) return existing.textContent.trim(); const toggleStats = () => { const evt = { ctrlKey: true, altKey: true, shiftKey: true, code: 'KeyS', key: 'S', bubbles:true, cancelable:true }; document.dispatchEvent(new KeyboardEvent('keydown', evt)); document.dispatchEvent(new KeyboardEvent('keyup', evt)); }; try { toggleStats(); } catch(e){} const start = Date.now(); while(Date.now() - start < timeoutMs) { const p = document.querySelector('p[aria-label="Задержка до владельца канала"]'); if (p && p.textContent.trim().length) { const container = p.closest('table, .tw-stat, div'); if (container) container.style.display = 'none'; try { toggleStats(); } catch(e){} return p.textContent.trim(); } await new Promise(r=>setTimeout(r,150)); } try { toggleStats(); } catch(e){} return null; } function readKickLatency() { const video = document.querySelector('video'); if (!video || !video.buffered.length) return null; const latency = video.buffered.end(video.buffered.length -1) - video.currentTime; return latency>0 ? latency.toFixed(2)+'s' : '0.00s'; } async function getLatency() { if (platform === 'kick') return readKickLatency(); const val = await readTwitchStats(); if (!val) { const video = document.querySelector('video'); if(video && video.buffered.length){ const lat = video.buffered.end(video.buffered.length-1)-video.currentTime; return lat>0 ? lat.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 = (num/1000); return num.toFixed(2)+'s'; } return val; } async function updateHeader() { if (!header) return; const latency = await getLatency(); if (!latency) return; header.innerHTML = ''; let dot = document.getElementById('latency-red-dot'); if(!dot) dot = createRedDot(); header.appendChild(dot); const span = document.createElement('span'); span.textContent = `Latency: ${latency}`; header.appendChild(span); } function createSpinner() { if (spinner) return spinner; let tpl = platform==='twitch' ? document.querySelector('[data-a-target="tw-loading-spinner"]') : document.querySelector('[data-testid="loading-spinner"]'); spinner = tpl ? tpl.cloneNode(true) : 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 video = document.querySelector('video'); if(video && video.parentElement) video.parentElement.appendChild(spinner); return spinner; } function reloadPlayer() { const video = document.querySelector('video'); if(!video) return; const sp = createSpinner(); sp.style.display='block'; const t = video.currentTime; video.pause(); setTimeout(()=>{ try{ video.currentTime = t; video.play().catch(()=>{}); } catch { location.reload(); } sp.style.display='none'; updateHeader(); },1200); } function findHeader() { if(header) return; let candidate; if(platform==='twitch') candidate=document.querySelector('#chat-room-header-label'); else candidate = Array.from(document.querySelectorAll('span.absolute')).find(el=>el.textContent.trim()==='Чат'); if(candidate){ header=candidate; styleHeader(header); header.addEventListener('click', reloadPlayer); updateHeader(); } } const observer = new MutationObserver(findHeader); observer.observe(document.body,{childList:true,subtree:true}); setInterval(()=>{ if(header) updateHeader(); },2000); })();