// ==UserScript== // @name Twitch & Kick Latency // @namespace latency // @version 1.5.15 // @description Displays Twitch & Kick latency with proper player reload and MiniPlayer, fixed slider and checkbox color, English Twitch support // @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(){ const pad=10,k='miniPlayerPos',sK='miniPlayerSize',dK='miniPlayerDraggable'; const platform=location.hostname.includes('kick.com')?'kick':'twitch'; let header=null,spinner=null,miniPlayer=null,gearBtn=null,menu=null; const link=document.createElement('link'); link.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'; link.rel='stylesheet'; document.head.appendChild(link); function getHeaderHeight(){ const sel=['.top-nav','[data-a-target="top-nav"]','.top-nav__menu','header','.tw-header']; for(let s of sel){let e=document.querySelector(s);if(e){let r=e.getBoundingClientRect();if(r.height>0)return r.bottom+pad;}} return 80+pad; } function styleHeader(el){ Object.assign(el.style,{display:'flex',alignItems:'center',justifyContent:'center',color:'#fff',fontWeight:'600',fontSize:'15px',cursor:'pointer',gap:'6px'}); } function createRedDot(){ const d=document.createElement('span'); d.id='latency-red-dot'; Object.assign(d.style,{display:'inline-block',width:'8px',height:'8px',borderRadius:'50%',background:'#FF4B4B'}); return d; } // ===== Исправленная функция для Twitch EN/ RU ===== async function readTwitchStats(timeoutMs=1500){ const labels = ['Задержка до владельца канала', 'Latency To Broadcaster']; let existing = null; for (let label of labels){ existing = document.querySelector(`p[aria-label="${label}"]`); if(existing) break; } if(existing) return existing.textContent.trim(); const toggle=()=>{ ['keydown','keyup'].forEach(t=>document.dispatchEvent( new KeyboardEvent(t,{ctrlKey:true,altKey:true,shiftKey:true,code:'KeyS',key:'S',bubbles:true,cancelable:true}) )); }; try{toggle();}catch{} const start=Date.now(); while(Date.now()-startsetTimeout(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/=1e3;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'; Object.assign(spinner.style,{position:'absolute',top:'50%',left:'50%',transform:'translate(-50%,-50%)',zIndex:'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; if(platform==='twitch'){ candidate = document.querySelector('#chat-room-header-label'); } else { candidate = Array.from(document.querySelectorAll('span.absolute')).find(e => { const text = e.textContent.trim().toLowerCase(); return text === 'чат' || text === 'chat'; }); } 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); // ===== MiniPlayer logic ===== function initializeMiniPlayer(){ miniPlayer = document.querySelector('.persistent-player__border--mini'); if(!miniPlayer) return false; if(!miniPlayer._originalWidth){ const currentScale = parseFloat(localStorage.getItem(sK)) || 1; miniPlayer._originalWidth = miniPlayer.offsetWidth / currentScale; miniPlayer._originalHeight = miniPlayer.offsetHeight / currentScale; } let saved = localStorage.getItem(k); if(saved){ try{ let pos = JSON.parse(saved); miniPlayer.style.left = pos.left + 'px'; miniPlayer.style.top = pos.top + 'px'; }catch{ miniPlayer.style.left = pad + 'px'; miniPlayer.style.top = getHeaderHeight() + 'px'; } } else { miniPlayer.style.left = pad + 'px'; miniPlayer.style.top = getHeaderHeight() + 'px'; } let savedSize = parseFloat(localStorage.getItem(sK)) || 1; miniPlayer.style.transform = `scale(${savedSize})`; if(miniPlayer._dragInitialized) return true; Object.assign(miniPlayer.style, { position: 'fixed', cursor: 'move', zIndex: '9999', margin: '0', transition: 'transform 0.2s ease', transformOrigin: 'top left' }); addGearToMiniPlayer(); let dragging=false, startX=0, startY=0, initLeft=0, initTop=0; miniPlayer.addEventListener('mousedown', e => { if(!miniPlayer.classList.contains('persistent-player__border--mini')) return; const draggable = localStorage.getItem(dK) !== 'false'; if(!draggable || e.target.closest('.mini-gear')) return; dragging = true; startX = e.clientX; startY = e.clientY; initLeft = parseFloat(miniPlayer.style.left); initTop = parseFloat(miniPlayer.style.top); miniPlayer.style.transition = 'none'; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); e.preventDefault(); e.stopPropagation(); }); function drag(e) { if (!dragging) return; let dx = e.clientX - startX; let dy = e.clientY - startY; const currentScale = parseFloat(localStorage.getItem(sK)) || 1; const rect = miniPlayer.getBoundingClientRect(); const scaledWidth = rect.width; const scaledHeight = rect.height; const newLeft = initLeft + dx; const newTop = initTop + dy; const minLeft = pad; const minTop = getHeaderHeight(); const maxLeft = window.innerWidth - scaledWidth - pad; const maxTop = window.innerHeight - scaledHeight - pad; miniPlayer.style.left = Math.min(Math.max(newLeft, minLeft), maxLeft) + 'px'; miniPlayer.style.top = Math.min(Math.max(newTop, minTop), maxTop) + 'px'; e.preventDefault(); } function stopDrag(){ if(!dragging) return; dragging = false; document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); localStorage.setItem(k, JSON.stringify({ left: Math.round(parseFloat(miniPlayer.style.left)), top: Math.round(parseFloat(miniPlayer.style.top)) })); miniPlayer.style.transition = 'transform 0.2s ease'; } let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { const currentScale = parseFloat(localStorage.getItem(sK)) || 1; const scaledWidth = miniPlayer._originalWidth * currentScale; const scaledHeight = miniPlayer._originalHeight * currentScale; let left = parseFloat(miniPlayer.style.left); let top = parseFloat(miniPlayer.style.top); let newL = Math.max(pad, Math.min(left, window.innerWidth - scaledWidth - pad)); let newT = Math.max(getHeaderHeight(), Math.min(top, window.innerHeight - scaledHeight - pad)); if(newL !== left || newT !== top){ miniPlayer.style.left = newL + 'px'; miniPlayer.style.top = newT + 'px'; localStorage.setItem(k, JSON.stringify({left: Math.round(newL), top: Math.round(newT)})); } }, 100); }); miniPlayer._dragInitialized = true; return true; } // ===== Gear & Menu ===== function addGearToMiniPlayer(){ if(!miniPlayer||!miniPlayer.classList.contains('persistent-player__border--mini')) return; document.querySelectorAll('.mini-gear').forEach(e=>e.remove()); gearBtn=document.createElement('button'); gearBtn.className='mini-gear'; Object.assign(gearBtn.style,{position:'absolute',top:'8px',right:'40px',background:'rgba(0,0,0,0.5)',border:'none',borderRadius:'4px',width:'24px',height:'24px',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',zIndex:'99999',opacity:'0',transition:'opacity 0.15s ease-in-out'}); gearBtn.innerHTML=``; miniPlayer.appendChild(gearBtn); miniPlayer.addEventListener('mouseenter',()=>{gearBtn.style.opacity='1';}); miniPlayer.addEventListener('mouseleave',()=>{gearBtn.style.opacity='0';}); gearBtn.addEventListener('click',toggleMenu); } // ===== Menu ===== function createMenu(){ if(menu) return; menu=document.createElement('div'); Object.assign(menu.style,{ position:'fixed',top:'50%',left:'50%',transform:'translate(-50%,-50%) scale(0.9)', background:'#18181b',color:'#fff',padding:'20px',borderRadius:'12px',zIndex:'10000', display:'none',fontFamily:'Inter,Arial,sans-serif',minWidth:'300px', boxShadow:'0 0 15px rgba(0,0,0,0.5)',transition:'transform 0.2s ease, opacity 0.2s ease',opacity:'0',cursor:'default' }); menu.innerHTML=`
Created by: frz
`; document.body.appendChild(menu); document.getElementById('close-menu').addEventListener('click',()=>{menu.style.opacity='0';menu.style.transform='translate(-50%,-50%) scale(0.9)';setTimeout(()=>{menu.style.display='none';},200);}); const slider=document.getElementById('miniplayer-size'); const display=document.getElementById('size-value'); const savedSize=parseFloat(localStorage.getItem(sK))||1; slider.value=savedSize; display.textContent=`${Math.round(savedSize*100)}%`; // ===== Фиолетовый цвет ползунка ===== slider.style.accentColor = '#9147ff'; let scaleTimeout; slider.addEventListener('input',e=>{ const scale=parseFloat(e.target.value); display.textContent=`${Math.round(scale*100)}%`; clearTimeout(scaleTimeout); scaleTimeout=setTimeout(()=>{ if(miniPlayer){ const r=miniPlayer.getBoundingClientRect(); localStorage.setItem(k,JSON.stringify({left:Math.round(r.left),top:Math.round(r.top)})); miniPlayer.style.transform=`scale(${scale})`; localStorage.setItem(sK,scale); } },150); }); const dragToggle=document.getElementById('drag-toggle'); const dragSaved=localStorage.getItem(dK); dragToggle.checked=dragSaved===null?true:dragSaved==='true'; // ===== Фиолетовый цвет галочки ===== dragToggle.style.accentColor = '#9147ff'; dragToggle.addEventListener('change',()=>{localStorage.setItem(dK,dragToggle.checked);}); // ===== Menu Drag ===== let isMenuDragging=false,offsetX=0,offsetY=0; const headerDrag=menu.querySelector('#menu-header'); headerDrag.addEventListener('mousedown',e=>{isMenuDragging=true;const rect=menu.getBoundingClientRect();offsetX=e.clientX-rect.left;offsetY=e.clientY-rect.top;menu.style.transition='none';e.preventDefault();}); document.addEventListener('mousemove',e=>{if(!isMenuDragging)return;let newX=e.clientX-offsetX;let newY=e.clientY-offsetY;newX=Math.max(0,Math.min(window.innerWidth-menu.offsetWidth,newX));newY=Math.max(0,Math.min(window.innerHeight-menu.offsetHeight,newY));menu.style.left=newX+'px';menu.style.top=newY+'px';menu.style.transform='translate(0,0)';}); document.addEventListener('mouseup',()=>{if(!isMenuDragging)return;isMenuDragging=false;menu.style.transition='transform 0.2s ease, opacity 0.2s ease';}); } function toggleMenu(){createMenu();if(!menu)return;if(menu.style.display==='block'){menu.style.opacity='0';menu.style.transform='translate(-50%,-50%) scale(0.9)';setTimeout(()=>{menu.style.display='none';},200);}else{menu.style.display='block';menu.style.opacity='1';menu.style.transform='translate(-50%,-50%) scale(1)';}} // ===== Player state check ===== function checkPlayerState(){ const mini = document.querySelector('.persistent-player__border--mini'); const full = document.querySelector('.persistent-player:not(.persistent-player__border--mini)'); if (full) { document.querySelectorAll('.mini-gear').forEach(e => e.remove()); if(menu && menu.style.display === 'block'){ menu.style.opacity='0'; menu.style.transform='translate(-50%,-50%) scale(0.9)'; setTimeout(()=>{menu.style.display='none';},200); } if (miniPlayer) { miniPlayer._dragInitialized = false; miniPlayer.style.cursor = 'default'; miniPlayer = null; } } else if (mini) { miniPlayer = mini; miniPlayer.style.cursor = 'move'; if (!mini.querySelector('.mini-gear')) { addGearToMiniPlayer(); initializeMiniPlayer(); } } } const playerObserver=new MutationObserver(()=>{checkPlayerState();}); playerObserver.observe(document.body,{childList:true,subtree:true,attributes:true,attributeFilter:['class']}); setInterval(checkPlayerState,1000); setTimeout(checkPlayerState,1000); })();