Better Kick DGG Embed

Enhanced DGG Kick embed with auto 1080p, auto-catchup to live, and toggleable catchup

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
  }
})();