Greasy Fork is available in English.
Displays Twitch & Kick latency with proper mini player drag
当前为
// ==UserScript==
// @name Twitch & Kick Latency
// @namespace latency
// @version 1.5.9
// @description Displays Twitch & Kick latency with proper mini player drag
// @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
// ==/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;
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;
}
async function readTwitchStats(timeoutMs=1500){
const existing=document.querySelector('p[aria-label="Задержка до владельца канала"]');
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()-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/=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=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);
function initializeMiniPlayer(){
if(!miniPlayer||!miniPlayer.classList.contains('persistent-player__border--mini')) return false;
function setDefaultPosition(){
let h=getHeaderHeight();
miniPlayer.style.left=pad+'px';
miniPlayer.style.top=h+'px';
miniPlayer.style.bottom='auto';
miniPlayer.style.right='auto';
}
let saved=localStorage.getItem(k);
if(saved){
try{
let pos=JSON.parse(saved);
let h=getHeaderHeight();
let maxL=window.innerWidth-miniPlayer.offsetWidth-pad;
let maxT=window.innerHeight-miniPlayer.offsetHeight-pad;
let safeTop=Math.max(h,Math.min(pos.top,maxT));
miniPlayer.style.left=Math.max(pad,Math.min(pos.left,maxL))+'px';
miniPlayer.style.top=safeTop+'px';
}catch{setDefaultPosition();}
}else setDefaultPosition();
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'});
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;
const 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(pad,Math.min(initLeft+dx,window.innerWidth-miniPlayer.offsetWidth-pad));
let newT=Math.max(getHeaderHeight(),Math.min(initTop+dy,window.innerHeight-miniPlayer.offsetHeight-pad));
miniPlayer.style.left=newL+'px';
miniPlayer.style.top=newT+'px';
}
function stopDrag(){
if(!dragging) return;
dragging=false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
const r=miniPlayer.getBoundingClientRect();
localStorage.setItem(k,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(pad,Math.min(r.left,window.innerWidth-miniPlayer.offsetWidth-pad));
let newT=Math.max(getHeaderHeight(),Math.min(r.top,window.innerHeight-miniPlayer.offsetHeight-pad));
if(newL!==r.left||newT!==r.top){miniPlayer.style.left=newL+'px'; miniPlayer.style.top=newT+'px';}
});
miniPlayer._dragInitialized=true;
return true;
}
function addGearToMiniPlayer(){
if(!miniPlayer||!miniPlayer.classList.contains('persistent-player__border--mini')) return;
document.querySelectorAll('.mini-gear').forEach(e=>e.remove());
if(miniPlayer.querySelector('.mini-gear')) return;
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=`<svg width="16" height="16" viewBox="0 0 20 20" fill="#fff"><path d="M10 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"></path><path fill-rule="evenodd" d="M9 2h2a2.01 2.01 0 0 0 1.235 1.855l.53.22a2.01 2.01 0 0 0 2.185-.439l1.414 1.414a2.01 2.01 0 0 0-.439 2.185l.22.53A2.01 2.01 0 0 0 18 9v2a2.01 2.01 0 0 0-1.855 1.235l-.22.53a2.01 2.01 0 0 0 .44 2.185l-1.415 1.414a2.01 2.01 0 0 0-2.184-.439l-.531.22A2.01 2.01 0 0 0 11 18H9a2.01 2.01 0 0 0-1.235-1.854l-.53-.22a2.009 2.009 0 0 0-2.185.438L3.636 14.95a2.009 2.009 0 0 0 .438-2.184l-.22-.531A2.01 2.01 0 0 0 2 11V9c.809 0 1.545-.487 1.854-1.235l.22-.53a2.009 2.009 0 0 0-.438-2.185L5.05 3.636a2.01 2.01 0 0 0 2.185.438l.53-.22A2.01 2.01 0 0 0 9 2zm-4 8 1.464 3.536L10 15l3.535-1.464L15 10l-1.465-3.536L10 5 6.464 6.464 5 10z" clip-rule="evenodd"></path></svg>`;
miniPlayer.appendChild(gearBtn);
miniPlayer.addEventListener('mouseenter', ()=>{ if(gearBtn) gearBtn.style.opacity='1'; });
miniPlayer.addEventListener('mouseleave', ()=>{ if(gearBtn) gearBtn.style.opacity='0'; });
gearBtn.addEventListener('click', toggleMenu);
}
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:'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=`<div id="menu-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;cursor:move;">
<h3 style="margin:0;font-size:16px;">Settings:</h3>
<span id="close-menu" style="cursor:pointer;font-size:18px;font-weight:bold;">✖</span>
</div>
<label style="display:block;margin-bottom:10px;font-size:14px;">MiniPlayer Size:
<input type="range" id="miniplayer-size" min="1" max="2" step="0.05" value="1" style="width:100%;margin-top:5px;">
<span id="size-value" style="margin-left:6px;">100%</span>
</label>
<label style="display:block;margin-bottom:10px;font-size:14px;">Enable MiniPlayer Drag:
<input type="checkbox" id="drag-toggle" style="margin-left:6px;">
</label>
<div style="text-align:right;font-size:12px;margin-top:10px;">Created by: frz</div>`;
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.addEventListener('input', e=>{
const scale=parseFloat(e.target.value);
if(miniPlayer) miniPlayer.style.transform=`scale(${scale})`;
display.textContent=`${Math.round(scale*100)}%`;
localStorage.setItem(sK,scale);
});
const dragToggle=document.getElementById('drag-toggle');
const dragSaved=localStorage.getItem(dK);
dragToggle.checked=dragSaved===null?true:dragSaved==='true';
dragToggle.addEventListener('change', ()=>{ localStorage.setItem(dK, dragToggle.checked); });
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)';
}
}
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(mini){
miniPlayer=mini;
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);
})();