Greasy Fork is available in English.
Draggable Twitch mini-player + Twitch/Kick latency display
当前为
// ==UserScript==
// @name Twitch Mini Player + Latency
// @namespace latency
// @version 1.5
// @description Draggable Twitch mini-player + Twitch/Kick latency display
// @match https://www.twitch.tv/*
// @match https://kick.com/*
// @grant none
// ==/UserScript==
(function() {
const padding = 10, key = 'miniPlayerPos';
const platform = location.hostname.includes('kick.com') ? 'kick' : 'twitch';
let header = null, spinner = null;
function getHeaderHeight() {
const selectors = ['.top-nav','[data-a-target="top-nav"]','.top-nav__menu','header','.tw-header'];
for (let s of selectors) {
let e = document.querySelector(s);
if (e) { let r = e.getBoundingClientRect(); if (r.height > 0) return r.bottom + padding; }
}
return 80 + padding;
}
function initializeMiniPlayer() {
const player = document.querySelector('.persistent-player');
const miniPlayer = document.querySelector('.persistent-player__border--mini');
if (!miniPlayer || !player) return false;
function setDefaultPosition() {
let h = getHeaderHeight();
miniPlayer.style.left = padding + 'px';
miniPlayer.style.top = h + 'px';
miniPlayer.style.bottom = 'auto';
miniPlayer.style.right = 'auto';
}
let saved = localStorage.getItem(key);
if (saved) {
try {
let pos = JSON.parse(saved);
let h = getHeaderHeight();
let maxL = window.innerWidth - miniPlayer.offsetWidth - padding;
let maxT = window.innerHeight - miniPlayer.offsetHeight - padding;
let safeTop = Math.max(h, Math.min(pos.top, maxT));
miniPlayer.style.left = Math.max(padding, Math.min(pos.left, maxL)) + 'px';
miniPlayer.style.top = safeTop + 'px';
} catch { setDefaultPosition(); }
} else setDefaultPosition();
if (miniPlayer._dragInitialized) return true;
let dragging = false, startX = 0, startY = 0, initLeft = 0, initTop = 0;
miniPlayer.style.position = 'fixed';
miniPlayer.style.cursor = 'move';
miniPlayer.style.zIndex = '9999';
miniPlayer.style.margin = '0';
miniPlayer.addEventListener('mousedown', e => {
if (e.target.closest('button') || e.target.closest('a')) return;
dragging = true; startX = e.clientX; startY = e.clientY;
let 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(padding, Math.min(initLeft + dx, window.innerWidth - miniPlayer.offsetWidth - padding));
let newT = Math.max(getHeaderHeight(), Math.min(initTop + dy, window.innerHeight - miniPlayer.offsetHeight - padding));
miniPlayer.style.left = newL + 'px';
miniPlayer.style.top = newT + 'px';
miniPlayer.style.bottom = miniPlayer.style.right = 'auto';
}
function stopDrag() {
if (!dragging) return;
dragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
let r = miniPlayer.getBoundingClientRect();
localStorage.setItem(key, 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(padding, Math.min(r.left, window.innerWidth - miniPlayer.offsetWidth - padding));
let newT = Math.max(getHeaderHeight(), Math.min(r.top, window.innerHeight - miniPlayer.offsetHeight - padding));
if (newL !== r.left || newT !== r.top) { miniPlayer.style.left = newL + 'px'; miniPlayer.style.top = newT + 'px'; }
});
miniPlayer._dragInitialized = true;
return true;
}
let attempts = 0, maxAttempts = 50;
const tryInit = setInterval(() => { if (initializeMiniPlayer() || attempts >= maxAttempts) clearInterval(tryInit); attempts++; }, 100);
new MutationObserver(() => { initializeMiniPlayer(); }).observe(document.body, { childList: true, subtree: true });
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 d = document.createElement('span');
d.id = 'latency-red-dot';
d.style.cssText = 'display:inline-block;width:8px;height:8px;border-radius: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 = () => {
const e = { ctrlKey: true, altKey: true, shiftKey: true, code: 'KeyS', key: 'S', bubbles: true, cancelable: true };
document.dispatchEvent(new KeyboardEvent('keydown', e));
document.dispatchEvent(new KeyboardEvent('keyup', e));
};
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 /= 1000; 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';
spinner.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index: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);
})();