Displays real-time stream latency next to viewer count on Kick.com with auto speed adjustment and Mo'Kick compatibility
// ==UserScript==
// @name Kick Stream Delay Display
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description Displays real-time stream latency next to viewer count on Kick.com with auto speed adjustment and Mo'Kick compatibility
// @description:pl Wyświetla opóźnienie streamu obok licznika widzów na Kick.com z auto-przyspieszaniem i zgodnością z Mo'Kick
// @author Premiumsmart
// @match https://kick.com/*
// @icon https://kick.com/favicon.ico
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// Opcjonalnie, zmień te wartości według własnych potrzeb
const targetDelay = 6; // Opóźnienie w sekundach, przy którym zmieniamy prędkość
const speedUpFactor = 1.25; // Współczynnik przyspieszenia
// Kick zmienił swój frontend i USUNĄŁ atrybut data-testid="viewer-count".
// Dlatego licznik widzów trzeba znaleźć strukturalnie. Rząd statystyk to:
// div.flex.items-center.gap-2.self-end (rodzic)
// └ div.flex.items-center.gap-1 (komórka: ikona + liczba) ← "Viewers"
// Mo'Kick DODAJE swoje komórki (Chatters / Participation) do TEGO SAMEGO rzędu,
// więc wstawienie badge jako pierwszej komórki działa identycznie z Mo'Kick i bez.
function getVideo() {
return document.querySelector('#video-player') || document.querySelector('video');
}
// Zwraca { row } — rząd statystyk, do którego wstawiamy badge (jako pierwsze dziecko).
function findStatsRow() {
// 0) Zgodność wstecz: jeśli Kick kiedyś przywróci testid.
const legacy = document.querySelector('[data-testid="viewer-count"]');
if (legacy) return legacy;
// A) Mo'Kick aktywny: pierwszy animowany licznik = widzowie (zielony text-primary-base).
// struktura: span.moKick-number-counter → div.text-*-base → div.komórka(svg) → div.rząd(self-end)
const moCounter = document.querySelector('.moKick-number-counter');
if (moCounter) {
const cell = moCounter.closest('div.flex.items-center.gap-1')
|| (moCounter.parentElement && moCounter.parentElement.parentElement);
if (cell && cell.parentElement) return cell.parentElement;
}
// B) Natywnie (bez Mo'Kick): rząd wyrównany do prawej (self-end) z ikoną i liczbą.
const scope = document.querySelector('#channel-content') || document;
const selfEndRows = scope.querySelectorAll('div[class*="self-end"]');
for (const row of selfEndRows) {
if (row.querySelector('svg') && /\d/.test(row.textContent)) return row;
}
// C) Ostateczny fallback: prawa kolumna metadanych obok tytułu streamu.
const title = document.querySelector('[data-testid="livestream-title"]');
if (title) {
let anc = title;
for (let i = 0; i < 6 && anc.parentElement; i++) anc = anc.parentElement;
const col = anc.querySelector('div[class*="items-end"]');
if (col) return col;
}
return null;
}
function createBadge(delayText) {
const badge = document.createElement('div');
badge.id = 'delay-display';
badge.className = 'flex items-center gap-1 text-sm font-bold relative';
// Style inline — nie polegamy na tym, że klasy Tailwind Kicka istnieją.
badge.style.display = 'flex';
badge.style.alignItems = 'center';
badge.style.gap = '4px';
badge.style.color = '#53FC18'; // Zielony kolor Kick
badge.style.fontWeight = 'bold';
badge.style.fontSize = '0.875rem';
badge.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1L12 6H10V12H6V6H4L8 1Z"></path>
</svg>
<span id="delay-text">${delayText}</span>
`;
return badge;
}
function updateDelayDisplay(delayText) {
const row = findStatsRow();
if (!row) return; // nie ma jeszcze paska statystyk — spróbujemy w następnym ticku
let badge = document.getElementById('delay-display');
if (!badge) {
badge = createBadge(delayText);
}
// Self-heal: zawsze upewnij się, że badge jest PIERWSZĄ komórką aktualnego rzędu.
// Kick (React) i Mo'Kick potrafią przebudować DOM i usunąć/przenieść nasz element.
if (badge.parentElement !== row || row.firstElementChild !== badge) {
row.insertBefore(badge, row.firstChild);
}
const textSpan = badge.querySelector('#delay-text');
if (textSpan) textSpan.textContent = delayText;
}
// Jeden tick: policz opóźnienie, dostosuj prędkość, spróbuj wyświetlić.
// Każdy krok w try/catch — jeden wyjątek NIE może zabić całej pętli (błąd oryginału v1.1).
function tick() {
let delayText = '';
try {
const video = getVideo();
if (video && video.buffered && video.buffered.length > 0) {
const currentTime = video.currentTime;
const bufferEnd = video.buffered.end(video.buffered.length - 1);
const delay = bufferEnd - currentTime;
if (delay > targetDelay) {
video.playbackRate = speedUpFactor;
} else {
video.playbackRate = 1.0; // Przywróć normalną prędkość
}
delayText = `${delay.toFixed(2)}s`;
}
} catch (e) {
// ignoruj — nie przerywaj pętli
}
try {
if (delayText) updateDelayDisplay(delayText);
} catch (e) {
// ignoruj — nie przerywaj pętli
}
}
// Poczekaj aż pojawi się wideo, potem uruchom stały interwał.
// setInterval (a nie łańcuch setTimeout) gwarantuje, że pętla żyje mimo błędów.
function start() {
console.log('Kick Stream Delay Display v1.2: Started');
tick();
setInterval(tick, 1000);
}
function waitForVideo() {
if (getVideo()) {
start();
} else {
setTimeout(waitForVideo, 500);
}
}
waitForVideo();
})();