Greasy Fork is available in English.
Displays the Twitch and Kick play latency
当前为
// ==UserScript==
// @name Twitch & Kick Latency
// @namespace latency
// @version 1.3
// @description Displays the Twitch and Kick play latency
// @match https://www.twitch.tv/*
// @match https://kick.com/*
// @grant 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); // 2 сек
})();