Enhanced DGG Kick embed with auto 1080p, auto-catchup to live, and toggleable catchup
// ==UserScript==
// @name Better Kick DGG Embed
// @namespace yuniDev.kickembed
// @version 1.21
// @description Enhanced DGG Kick embed with auto 1080p, auto-catchup to live, and toggleable catchup
// @author yunIDev and Cyclone
// @match *://*.kick.com/*
// @match https://www.destiny.gg/bigscreen*
// @match https://destiny.gg/bigscreen*
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.addStyle
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const DEBUG_MODE = false;
const DEFAULT_QUALITY = 1080;
const isKickEmbed =
window.location.hostname === "kick.com" && window.self !== window.top;
const isDGG = window.location.pathname.startsWith("/bigscreen");
const isKickPage = window.location.hostname === "kick.com";
const SETTINGS_KEYS = {
overlayEnabled: "kick-embed.overlayEnabled",
autoCatchupEnabled: "kick-embed.autoCatchupEnabled",
};
const SESSION_QUALITY_KEY = "stream_quality";
const PROXY_URL = "https://corsproxy.io/?";
function debugLog(...args) {
if (!DEBUG_MODE) return;
console.log(...args);
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function ensureNumber(value, fallback) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}
async function getOrInitValue(key, defaultValue) {
const existing = await GM.getValue(key, undefined);
if (existing === undefined) {
await GM.setValue(key, defaultValue);
return defaultValue;
}
return existing;
}
function setTextIfChanged(el, next) {
if (!el) return false;
const v = next ?? "";
if (el.textContent === v) return false;
el.textContent = v;
return true;
}
function setAttrIfChanged(el, name, next) {
if (!el) return false;
const v = next ?? "";
if (el.getAttribute(name) === v) return false;
el.setAttribute(name, v);
return true;
}
function createDebugOverlay() {
if (!DEBUG_MODE) return null;
GM.addStyle(`
.kick-embed-debug{
position:absolute;
top:8px;
left:8px;
z-index:2147483647;
padding:8px 10px;
background:rgba(0,0,0,.72);
color:#fff;
font:12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
border:1px solid rgba(255,255,255,.18);
border-radius:6px;
pointer-events:none;
white-space:pre;
user-select:none;
}
`);
const el = document.createElement("div");
el.className = "kick-embed-debug";
el.textContent = "Kick Stream Optimizer: waiting for video…";
return el;
}
function getOverlayHost(video) {
return (
video.closest("#injected-channel-player") ||
video.closest('[data-testid="video-player"]') ||
video.parentElement ||
document.body
);
}
function attachOverlayToPlayer(video, overlayEl) {
if (!DEBUG_MODE || !overlayEl || !video) return;
const host = getOverlayHost(video);
if (!host) return;
if (overlayEl.parentElement !== host) {
overlayEl.remove();
host.appendChild(overlayEl);
}
const cs = window.getComputedStyle(host);
if (cs.position === "static") {
host.style.position = "relative";
}
}
function initKickUiStyles() {
if (!isKickEmbed) return;
GM.addStyle(`
#nav-main,#sidebar,aside,.main-header,#channel-content,#channel-chatroom,
.z-controls.absolute.right-7.top-7,
button[data-testid="video-player-clip"],
button[data-testid="video-player-theatre-mode"]{display:none!important}
main,.flex-grow,.flex-col{background:#000!important;padding:0!important;margin:0!important}
[data-radix-popper-content-wrapper]:has(.z-dropdown){
z-index:999999!important;
transform-style:preserve-3d!important
}
.z-dropdown{z-index:999999!important}
#injected-channel-player,#injected-embedded-channel-player-video{
position:fixed!important;top:0!important;left:0!important;
width:100vw!important;height:100vh!important;
z-index:99999!important;max-height:none!important;max-width:none!important;
background:#000!important;
transform:translateZ(0);will-change:transform
}
video#video-player{
width:100%!important;height:100%!important;
transform:translateZ(0);will-change:transform
}
.z-controls.bottom-0{
display:flex!important;
opacity:0!important;
pointer-events:none!important;
transition:none!important;
z-index:100000!important
}
#injected-channel-player:hover .z-controls.bottom-0,
#injected-embedded-channel-player-video:hover .z-controls.bottom-0,
div:has(> video):hover .z-controls.bottom-0{
opacity:1!important;
pointer-events:auto!important
}
[data-kick-auto-catchup-toggle]{
pointer-events:auto!important;
display:inline-flex!important;
align-items:center!important;
justify-content:center!important;
height:2rem!important;
width:2rem!important;
margin-right:.375rem!important;
padding:0!important;
border:none!important;
border-radius:.375rem!important;
background:transparent!important;
color:rgba(255,255,255,0.75)!important;
cursor:pointer!important;
appearance:none!important;
outline:none!important;
transition:color 0.2s ease!important;
}
[data-kick-auto-catchup-toggle][data-enabled="true"]{
color:#53fc18!important;
}
[data-kick-auto-catchup-toggle][data-enabled="false"]{
color:rgba(255,255,255,0.75)!important;
}
[data-kick-auto-catchup-toggle] svg{
width:1rem!important;
height:1rem!important;
fill:currentColor!important;
}
html,body{overflow:hidden!important;background:#000!important;margin:0!important;padding:0!important}
`);
}
const OVERLAY_ID = "custom-kick-overlay";
const overlayState = {
cachedViewerCount: "0",
livestreamId: null,
currentUsername: null,
cachedChannel: {
displayName: "Streamer",
title: "No Title",
avatar: "",
category: "Gaming",
url: "",
},
lastRendered: null,
};
const catchupState = {
enabled: true,
};
function initCustomOverlayStyles() {
if (!isKickPage) return;
GM.addStyle(`
#${OVERLAY_ID}{
position:absolute;
top:0;left:0;right:0;
z-index:100001;
pointer-events:none;
font-family:Inter,ui-sans-serif,system-ui,sans-serif
}
#${OVERLAY_ID},#${OVERLAY_ID} *{
background:transparent!important;
padding:initial!important;
margin:initial!important
}
#${OVERLAY_ID} a,#${OVERLAY_ID} svg,#${OVERLAY_ID} img{
pointer-events:auto
}
#${OVERLAY_ID} .kick-overlay-topbar{
opacity:0;
transition:opacity .3s ease!important
}
#injected-channel-player:hover #${OVERLAY_ID} .kick-overlay-topbar,
#injected-embedded-channel-player-video:hover #${OVERLAY_ID} .kick-overlay-topbar,
div:has(> video):hover #${OVERLAY_ID} .kick-overlay-topbar{
opacity:1
}
`);
}
async function fetchChannelInfo(username) {
const encodedUsername = encodeURIComponent(username);
const resp = await fetch(
`https://kick.com/api/v2/channels/${encodedUsername}/info`,
{ credentials: "include" },
);
if (!resp.ok) throw new Error(`Failed to fetch channel info: ${resp.status}`);
return await resp.json();
}
async function fetchViewerCount(livestreamId) {
const resp = await fetch(
`https://kick.com/current-viewers?ids[]=${encodeURIComponent(livestreamId)}`,
{ credentials: "include" },
);
if (!resp.ok) throw new Error(`Failed to fetch viewer count: ${resp.status}`);
return await resp.json();
}
function getUsernameFromUrl() {
const p = window.location.pathname || "/";
const u = p.split("/")[1];
return u || null;
}
function getPlayerContainerFromVideo(video) {
return (
document.getElementById("injected-channel-player") ||
document.getElementById("injected-embedded-channel-player-video") ||
video.closest(".video-container") ||
video.closest('[class*="player"]') ||
video.parentElement
);
}
function ensureOverlayMounted() {
const video = document.querySelector("video");
if (!video) return null;
const container = getPlayerContainerFromVideo(video);
if (!container) return null;
const cs = window.getComputedStyle(container);
if (cs.position === "static") container.style.position = "relative";
let overlay = document.getElementById(OVERLAY_ID);
if (!overlay) {
overlay = document.createElement("div");
overlay.id = OVERLAY_ID;
container.appendChild(overlay);
} else if (overlay.parentElement !== container) {
overlay.remove();
container.appendChild(overlay);
}
if (!overlay.__kickRefs) {
overlay.innerHTML = `
<div class="kick-overlay-topbar z-controls absolute left-0 right-0 top-0 flex items-start justify-between gap-4 h-40 px-6 py-4 transition-opacity duration-300"
style="background:linear-gradient(to bottom, rgba(10,10,10,.95), transparent)!important; padding:1rem 1.5rem 3rem!important;">
<div class="flex min-w-0 flex-1 flex-col items-start gap-1"
style="background:transparent!important; gap:.25rem!important;">
<div class="flex items-center gap-3" style="background:transparent!important; gap:.75rem!important;">
<a data-role="left-link" href="#" target="_blank" rel="noreferrer"
class="flex items-center gap-3"
style="background:transparent!important; gap:.75rem!important; display:flex!important; align-items:center!important;">
<svg viewBox="0 0 80 26" class="h-8 w-20 shrink-0" fill="#53fc18"
style="height:2rem;width:5rem;flex-shrink:0;">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 0H8.57143V5.71429H11.4286V2.85714H14.2857V0H22.8571V8.57143H20V11.4286H17.1429V14.2857H20V17.1429H22.8571V25.7143H14.2857V22.8571H11.4286V20H8.57143V25.7143H0V0ZM57.1429 0H65.7143V5.71429H68.5714V2.85714H71.4286V0H80V8.57143H77.1429V11.4286H74.2857V14.2857H77.1429V17.1429H80V25.7143H71.4286V22.8571H68.5714V20H65.7143V25.7143H57.1429V0ZM25.7143 0H34.2857V25.7143H25.7143V0ZM45.7143 0H40V2.85714H37.1429V22.8571H40V25.7143H45.7143H54.2857V17.1429H45.7143V8.57143H54.2857V0H45.7143Z">
</path>
</svg>
<div aria-hidden="true"
style="background-color:rgba(255,255,255,.16)!important;height:1.5rem;width:1px;flex-shrink:0;"></div>
<span data-role="category"
style="background:transparent!important;color:#53fc18!important;font-size:1rem;font-weight:600;"
class="truncate text-base font-semibold">
Gaming
</span>
</a>
</div>
<p data-role="title" class="line-clamp-2 text-sm"
style="background:transparent!important;color:rgba(255,255,255,.9)!important;font-size:.875rem;line-height:1.25rem;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">
No Title
</p>
</div>
<div class="flex shrink-0 flex-col items-end"
style="background:transparent!important;display:flex;flex-direction:column;align-items:flex-end;flex-shrink:0;">
<div class="flex items-center gap-2"
style="background:transparent!important;display:flex;align-items:center;gap:.5rem;">
<a data-role="right-link" href="#" target="_blank" rel="noreferrer"
style="background:transparent!important;display:flex;align-items:center;gap:.5rem;">
<img data-role="avatar" draggable="false" alt="avatar"
style="height:2.25rem;width:2.25rem;border-radius:9999px;border:2px solid transparent;object-fit:cover;flex-shrink:0; display:none;">
<span data-role="displayName"
style="background:transparent!important;color:#fff!important;font-size:1rem;font-weight:700;">
Streamer
</span>
<span
style="background-color:#53fc18!important;color:#000!important;display:inline-flex;border-radius:.125rem;padding:0 .25rem;font-size:.875rem;font-weight:500;">
LIVE
</span>
</a>
</div>
<div style="background:transparent!important;display:flex;align-items:center;gap:.25rem;font-size:.875rem;margin-top:.25rem;">
<span data-role="viewers"
style="background:transparent!important;color:#53fc18!important;font-weight:700;">
0 watching
</span>
</div>
</div>
</div>
`;
overlay.__kickRefs = {
leftLink: overlay.querySelector('[data-role="left-link"]'),
rightLink: overlay.querySelector('[data-role="right-link"]'),
category: overlay.querySelector('[data-role="category"]'),
title: overlay.querySelector('[data-role="title"]'),
avatar: overlay.querySelector('[data-role="avatar"]'),
displayName: overlay.querySelector('[data-role="displayName"]'),
viewers: overlay.querySelector('[data-role="viewers"]'),
};
}
return overlay;
}
function applyOverlayData(next) {
const overlay = ensureOverlayMounted();
if (!overlay) return;
const refs = overlay.__kickRefs;
if (!refs) return;
const prev = overlayState.lastRendered;
const shouldUpdate =
!prev ||
prev.url !== next.url ||
prev.displayName !== next.displayName ||
prev.title !== next.title ||
prev.avatar !== next.avatar ||
prev.category !== next.category ||
prev.viewers !== next.viewers;
if (!shouldUpdate) return;
const onlyViewersChanged =
prev &&
prev.viewers !== next.viewers &&
prev.url === next.url &&
prev.displayName === next.displayName &&
prev.title === next.title &&
prev.avatar === next.avatar &&
prev.category === next.category;
if (onlyViewersChanged) {
setTextIfChanged(refs.viewers, `${next.viewers} watching`);
overlayState.lastRendered = next;
return;
}
setAttrIfChanged(refs.leftLink, "href", next.url);
setAttrIfChanged(refs.rightLink, "href", next.url);
setAttrIfChanged(refs.leftLink, "title", next.displayName);
setAttrIfChanged(refs.rightLink, "title", next.displayName);
setTextIfChanged(refs.category, next.category);
setTextIfChanged(refs.title, next.title);
setTextIfChanged(refs.displayName, next.displayName);
if (next.avatar) {
if (refs.avatar.style.display === "none") refs.avatar.style.display = "";
setAttrIfChanged(refs.avatar, "src", next.avatar);
setAttrIfChanged(refs.avatar, "alt", `${next.displayName} avatar`);
} else {
if (refs.avatar.style.display !== "none") refs.avatar.style.display = "none";
setAttrIfChanged(refs.avatar, "src", "");
setAttrIfChanged(refs.avatar, "alt", "avatar");
}
setTextIfChanged(refs.viewers, `${next.viewers} watching`);
overlayState.lastRendered = next;
}
async function pollOverlayDataAndUpdate() {
const username = getUsernameFromUrl();
if (!username) return;
if (overlayState.currentUsername !== username) {
overlayState.currentUsername = username;
overlayState.livestreamId = null;
overlayState.cachedViewerCount = "0";
overlayState.cachedChannel = {
displayName: "Streamer",
title: "No Title",
avatar: "",
category: "Gaming",
url: window.location.href,
};
overlayState.lastRendered = null;
}
try {
const info = await fetchChannelInfo(username);
const displayName =
info?.user?.username ||
info?.user?.name ||
info?.channel?.user?.username ||
username;
const title =
info?.livestream?.session_title ||
info?.livestream?.title ||
"No Title";
const category =
info?.livestream?.categories?.[0]?.name ||
info?.livestream?.category?.name ||
"Gaming";
const avatar =
info?.user?.profile_pic ||
info?.user?.profile_picture ||
info?.channel?.user?.profile_pic ||
"";
overlayState.cachedChannel = {
displayName: displayName || username,
title: title || "No Title",
avatar: avatar || "",
category: category || "Gaming",
url: window.location.href,
};
const lsId = info?.livestream?.id;
if (lsId !== undefined && lsId !== null) overlayState.livestreamId = lsId;
if (overlayState.livestreamId) {
const viewerData = await fetchViewerCount(overlayState.livestreamId);
if (Array.isArray(viewerData) && viewerData.length > 0) {
const row = viewerData.find(
(x) => x?.livestream_id === overlayState.livestreamId,
);
if (row && row.viewers !== undefined && row.viewers !== null) {
overlayState.cachedViewerCount = String(row.viewers);
}
}
}
} catch (e) {
console.error("[Kick Overlay] poll failed:", e);
}
const next = {
url: overlayState.cachedChannel.url || window.location.href,
displayName: overlayState.cachedChannel.displayName || "Streamer",
title: overlayState.cachedChannel.title || "No Title",
avatar: overlayState.cachedChannel.avatar || "",
category: overlayState.cachedChannel.category || "Gaming",
viewers: overlayState.cachedViewerCount || "0",
};
applyOverlayData(next);
}
function startCustomOverlay() {
const pollMs = 30000;
const mountTick = setInterval(() => {
const overlay = ensureOverlayMounted();
if (overlay && overlayState.lastRendered) {
applyOverlayData(overlayState.lastRendered);
}
}, 2000);
setTimeout(() => clearInterval(mountTick), 120000);
pollOverlayDataAndUpdate();
setInterval(pollOverlayDataAndUpdate, pollMs);
}
const state = {
lastPath: "",
insertedPlayer: null,
};
class KickEmbedIframeWrapper extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<link rel="preconnect" href="https://kick.com">
<link rel="dns-prefetch" href="https://kick.com">
<iframe
is="x-frame-bypass"
style="width:100%;height:100%;border:none;transform:translateZ(0)"
class="embed-frame"
src=""
proxy="${PROXY_URL}"
allow="autoplay; fullscreen; encrypted-media; picture-in-picture; web-share"
allowfullscreen
loading="eager"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation">
</iframe>
`;
this.iframe = shadowRoot.querySelector("iframe");
}
static get observedAttributes() {
return ["src"];
}
connectedCallback() {
this.updateSrc();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "src" && oldValue !== newValue) {
this.updateSrc();
}
}
updateSrc() {
const src = this.getAttribute("src");
if (src && this.iframe && this.iframe.src !== src) {
this.iframe.src = src;
}
}
}
if (isDGG) {
customElements.define("kick-embed-iframe-wrapper", KickEmbedIframeWrapper);
}
function htmlToNode(html) {
const template = document.createElement("template");
template.innerHTML = html.trim();
return template.content.firstChild;
}
function addObserver(
selector,
callback = (el) => {
el.style.display = "none";
},
) {
const element = document.querySelector(selector);
if (element) {
callback(element);
return;
}
const observer = new MutationObserver((_, obs) => {
const el = document.querySelector(selector);
if (el) {
callback(el);
obs.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), 10000);
}
function extractChannel(iframeLocation) {
if (iframeLocation.includes("player.kick.com")) {
return iframeLocation.split("/").pop();
}
if (window.location.hash.startsWith("#kick/")) {
return window.location.hash.split("/")[1];
}
return null;
}
function buildKickUrl(channel) {
return `https://kick.com/${channel}?autoplay=true`;
}
function hideSurroundings() {
const selectors = [
{
sel: "[data-sidebar]",
cb: (el) => {
el.setAttribute("data-sidebar", "false");
el.setAttribute("data-theatre", "true");
el.setAttribute("data-chat", "false");
},
},
{
sel: ".z-controls.hidden button",
cb: (el) => {
el.parentNode.style.display = "none";
},
},
{ sel: "#channel-chatroom > div:first-child" },
{ sel: "#channel-content" },
{ sel: '.z-modal:has(button[data-testid="accept-cookies"])' },
{
sel: 'button[data-testid="mature"]',
cb: (btn) => btn.click(),
},
];
selectors.forEach(({ sel, cb }) => addObserver(sel, cb));
}
function fixVideoPlayer() {
const processedVideos = new WeakSet();
const playAttempts = new WeakMap();
const videoObserver = new MutationObserver(() => {
const videos = document.querySelectorAll("video");
videos.forEach((video) => {
if (processedVideos.has(video)) return;
processedVideos.add(video);
video.autoplay = true;
video.playsInline = true;
let playTimeout;
const attemptPlay = (reason = "unknown") => {
clearTimeout(playTimeout);
playTimeout = setTimeout(() => {
if (video.readyState >= 2 && !video.seeking && !video.ended) {
if (!video.paused) return;
const attempts = playAttempts.get(video) || 0;
if (attempts > 10) {
console.warn(
"[Kick Embed] Max play attempts reached, backing off",
);
playAttempts.delete(video);
return;
}
playAttempts.set(video, attempts + 1);
video
.play()
.then(() => playAttempts.delete(video))
.catch((err) => {
if (!err?.message?.includes("aborted")) {
debugLog(`[Kick Embed] Play failed (${reason}):`, err);
}
});
}
}, 100);
};
video.addEventListener(
"pause",
(e) => {
if (e.isTrusted && video.currentTime > 0) return;
if (video.readyState >= 2 && !video.seeking && !video.ended) {
attemptPlay("pause");
}
},
{ passive: true },
);
video.addEventListener("waiting", () => attemptPlay("buffering"), {
passive: true,
});
setTimeout(() => {
if (video.paused && video.readyState >= 2) {
attemptPlay("initial");
}
}, 500);
});
});
videoObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
document.querySelectorAll("video").forEach((v) => {
if (v.paused && v.readyState >= 2) v.play().catch(() => {});
});
}, 1000);
}
function initKickEmbed() {
initKickUiStyles();
initCustomOverlayStyles();
hideSurroundings();
fixVideoPlayer();
let lastResize = 0;
const resizeInterval = setInterval(() => {
const now = Date.now();
if (now - lastResize < 900) return;
lastResize = now;
const player = document.getElementById("injected-channel-player");
if (player) {
let parent = player.parentElement;
while (parent && parent.tagName !== "BODY") {
Array.from(parent.children).forEach((child) => {
if (!child.contains(player)) child.style.display = "none";
});
parent = parent.parentElement;
}
}
window.dispatchEvent(new Event("resize"));
}, 1000);
const navCheckInterval = setInterval(() => {
if (document.querySelector('nav:not([style*="display: none"])')) {
hideSurroundings();
}
}, 200);
return () => {
clearInterval(resizeInterval);
clearInterval(navCheckInterval);
};
}
function updateEmbed() {
const offlineImage = document.querySelector("#embed > .offline-image");
if (offlineImage && offlineImage.offsetParent) return;
if (state.insertedPlayer) return;
const iframe = document.querySelector("iframe.embed-frame");
if (!iframe) return;
const channel = extractChannel(iframe.src);
if (!channel) return;
const kickUrl = buildKickUrl(channel);
state.insertedPlayer = htmlToNode(
`<kick-embed-iframe-wrapper
class="embed-frame"
style="display:block"
src="${kickUrl}">
</kick-embed-iframe-wrapper>`,
);
iframe.parentNode.appendChild(state.insertedPlayer);
}
function loadDGG() {
const script = htmlToNode(
'<script type="module" src="https://unpkg.com/x-frame-bypass"></script>',
);
document.head.appendChild(script);
const embedContainer = document.getElementById("embed");
if (!embedContainer) {
console.warn("Embed container not found");
return () => {};
}
const embedObserver = new MutationObserver((mutations) => {
const offlineImage = embedContainer.querySelector(".offline-image");
if (offlineImage && offlineImage.offsetParent) {
state.insertedPlayer?.remove();
state.insertedPlayer = null;
}
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
for (const node of mutation.addedNodes) {
if (
node.nodeType === Node.ELEMENT_NODE &&
node.tagName === "IFRAME" &&
node.classList.contains("embed-frame")
) {
updateEmbed();
}
}
}
if (state.lastPath === window.location.href) {
const iframe = document.querySelector("iframe.embed-frame");
if (
iframe &&
iframe.src !== "about:blank?player.kick" &&
iframe.src.includes("player.kick.com")
) {
iframe.src = "about:blank?player.kick";
}
}
});
embedObserver.observe(embedContainer, {
childList: true,
subtree: true,
attributes: true,
});
updateEmbed();
return () => embedObserver.disconnect();
}
function initDGG() {
let disconnect = loadDGG();
state.lastPath = window.location.href;
const handleHashChange = () => {
setTimeout(() => {
disconnect();
state.insertedPlayer?.remove();
state.insertedPlayer = null;
state.lastPath = window.location.href;
disconnect = loadDGG();
}, 1);
};
window.addEventListener("hashchange", handleHashChange);
GM.addStyle('iframe[src*="player.kick"].embed-frame{display:none!important}');
}
function updateCatchupToggleButtons() {
const enabled = !!catchupState.enabled;
document
.querySelectorAll("[data-kick-auto-catchup-toggle]")
.forEach((btn) => {
btn.setAttribute("data-enabled", String(enabled));
btn.setAttribute("aria-pressed", String(enabled));
btn.setAttribute(
"title",
enabled ? "Disable auto catch-up" : "Enable auto catch-up",
);
});
}
async function setAutoCatchupEnabled(enabled) {
catchupState.enabled = !!enabled;
updateCatchupToggleButtons();
try {
await GM.setValue(
SETTINGS_KEYS.autoCatchupEnabled,
catchupState.enabled,
);
} catch (e) {
console.error("[Kick Embed] Failed to save auto catch-up setting:", e);
}
}
function ensureCatchupToggleMounted() {
const controls =
document.querySelector("#injected-channel-player .z-controls.bottom-0") ||
document.querySelector(
"#injected-embedded-channel-player-video .z-controls.bottom-0",
) ||
document.querySelector(".z-controls.bottom-0");
if (!controls) return null;
const rows = Array.from(controls.children).filter(
(el) =>
el instanceof HTMLElement &&
el.tagName === "DIV" &&
window.getComputedStyle(el).position !== "absolute",
);
const rightRow = rows[rows.length - 1];
if (!rightRow) return null;
let btn = rightRow.querySelector("[data-kick-auto-catchup-toggle]");
if (!btn) {
btn = document.createElement("button");
btn.type = "button";
btn.setAttribute("data-kick-auto-catchup-toggle", "");
btn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
<path d="M1 3v26l14-13L1 3zm15 0v26l15-13L16 3z" />
</svg>
`;
btn.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
await setAutoCatchupEnabled(!catchupState.enabled);
const video = document.querySelector("video");
if (video) video.playbackRate = 1.0;
});
rightRow.insertBefore(btn, rightRow.firstChild);
}
updateCatchupToggleButtons();
return btn;
}
function startCatchupToggleUi() {
let rafId = 0;
const scheduleMount = () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = 0;
ensureCatchupToggleMounted();
});
};
scheduleMount();
const observer = new MutationObserver(() => {
scheduleMount();
});
observer.observe(document.body, { childList: true, subtree: true });
}
function startQualityLock() {
const targetValue = JSON.stringify(DEFAULT_QUALITY);
sessionStorage.setItem(SESSION_QUALITY_KEY, targetValue);
const originalSetItem = Storage.prototype.setItem;
if (!Storage.prototype.setItem.__kickOptimizerPatched) {
Storage.prototype.setItem = function (key, value) {
const currentTargetValue =
Storage.prototype.setItem.__kickOptimizerTargetValue ?? targetValue;
if (key === SESSION_QUALITY_KEY && value !== currentTargetValue) {
return originalSetItem.call(this, key, currentTargetValue);
}
return originalSetItem.apply(this, arguments);
};
Storage.prototype.setItem.__kickOptimizerPatched = true;
}
Storage.prototype.setItem.__kickOptimizerTargetValue = targetValue;
debugLog("[Kick Stream Optimizer] Quality locked to", DEFAULT_QUALITY);
}
function startDelayCompensation(debugEl) {
const KP = 0.55;
const KD = 0.12;
const DEAD_BAND = 0.1;
const MIN_RATE = 1.0;
const MAX_RATE = 2.0;
const SMOOTHING = 0.25;
const CATCHUP_UNDERSHOOT = 0.15;
const TARGET_DELAY = 1.0;
const catchUpTarget = TARGET_DELAY - CATCHUP_UNDERSHOOT;
let lastError = 0;
let lastTick = performance.now();
const tick = () => {
const video = document.querySelector("video");
if (!video || video.readyState < 2) {
setTimeout(tick, 800);
return;
}
attachOverlayToPlayer(video, debugEl);
if (!catchupState.enabled) {
if (video.playbackRate !== 1.0) video.playbackRate = 1.0;
if (DEBUG_MODE && debugEl) {
debugEl.textContent =
`saved res: ${DEFAULT_QUALITY}\n` +
`auto catchup: off\n` +
`playbackRate: ${video.playbackRate.toFixed(3)}\n`;
}
setTimeout(tick, 400);
return;
}
let bufferAhead = null;
try {
const buffered = video.buffered;
if (buffered.length > 0) {
bufferAhead = buffered.end(buffered.length - 1) - video.currentTime;
}
} catch (_) {}
if (bufferAhead === null || !Number.isFinite(bufferAhead)) {
setTimeout(tick, 500);
return;
}
const now = performance.now();
const dt = Math.max(0.2, Math.min(2.0, (now - lastTick) / 1000));
lastTick = now;
const isBehindTarget = bufferAhead - TARGET_DELAY > DEAD_BAND;
const controlTarget = isBehindTarget ? catchUpTarget : TARGET_DELAY;
const error = bufferAhead - controlTarget;
const dError = (error - lastError) / dt;
lastError = error;
let desiredRate = 1.0;
if (error > DEAD_BAND) {
desiredRate = 1 + KP * error + KD * dError;
}
desiredRate = clamp(desiredRate, MIN_RATE, MAX_RATE);
const currentRate = Math.max(1.0, ensureNumber(video.playbackRate, 1.0));
const nextRate =
desiredRate > currentRate
? currentRate + (desiredRate - currentRate) * SMOOTHING
: desiredRate;
video.playbackRate = clamp(nextRate, MIN_RATE, MAX_RATE);
if (DEBUG_MODE && debugEl) {
const distToTargetSigned = bufferAhead - TARGET_DELAY;
debugEl.textContent =
`saved res: ${DEFAULT_QUALITY}\n` +
`auto catchup: on\n` +
`buffer front distance: ${bufferAhead.toFixed(2)}s\n` +
`target delay: ${TARGET_DELAY.toFixed(2)}s\n` +
`catch-up undershoot: ${CATCHUP_UNDERSHOOT.toFixed(2)}s\n` +
`control target: ${controlTarget.toFixed(2)}s\n` +
`to target: ${distToTargetSigned.toFixed(2)}s\n` +
`playbackRate: ${video.playbackRate.toFixed(3)}\n`;
}
setTimeout(tick, 400);
};
tick();
}
function waitForVideo(onReady) {
const video = document.querySelector("video");
if (video) onReady();
else setTimeout(() => waitForVideo(onReady), 500);
}
async function init() {
const debugEl = createDebugOverlay();
const overlayEnabled = await getOrInitValue(SETTINGS_KEYS.overlayEnabled, true);
const autoCatchupEnabled = await getOrInitValue(
SETTINGS_KEYS.autoCatchupEnabled,
true,
);
catchupState.enabled = !!autoCatchupEnabled;
startQualityLock();
if (isKickEmbed) {
initKickEmbed();
startCatchupToggleUi();
if (overlayEnabled) startCustomOverlay();
waitForVideo(() => {
startDelayCompensation(debugEl);
});
} else if (isDGG) {
initDGG();
} else if (isKickPage) {
initCustomOverlayStyles();
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init, {
once: true,
passive: true,
});
} else {
init();
}
})();