Greasy Fork

Greasy Fork is available in English.

Twitch Mini Player + Latency

Draggable Twitch mini-player + Twitch/Kick latency display

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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);

})();