Greasy Fork

Greasy Fork is available in English.

Twitch & Kick Latency

Displays Twitch & Kick latency with proper player reload and MiniPlayer

当前为 2025-10-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Twitch & Kick Latency
// @namespace    latency
// @version      1.5.12
// @description  Displays Twitch & Kick latency with proper player reload and MiniPlayer
// @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;
 
 
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;
}
 
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(){
    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;
}
 
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=`<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',()=>{gearBtn.style.opacity='1';});
    miniPlayer.addEventListener('mouseleave',()=>{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:'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=`<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;font-style:italic;">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)}%`;
 
    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.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 (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);
})();