YouTube Custom Speed Control Button

Injects a custom speed control button into the YouTube player

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         YouTube Custom Speed Control Button
// @namespace    http://tampermonkey.net/
// @version      2026-04-30
// @description  Injects a custom speed control button into the YouTube player
// @author       You
// @match        *://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const trustedPolicy =
    window.trustedTypes && window.trustedTypes.createPolicy
      ? window.trustedTypes.createPolicy("youtube-speed-btn-policy", {
          createHTML: (string) => string,
        })
      : null;

  function injectSpeedButton() {
    const buttonId = "custom-speed-btn";

    // Prevent duplicate buttons
    if (document.getElementById(buttonId)) return;

    const rightControlsLeft = document.querySelector(
      ".ytp-right-controls .ytp-right-controls-left",
    );
    if (!rightControlsLeft) return;

    const getCurrentVideo = () =>
      document.querySelector("video.html5-main-video") ??
      document.querySelector("video");
    if (!getCurrentVideo()) return;

    // Create the button
    const btn = document.createElement("button");
    btn.id = buttonId;
    btn.className = "ytp-button"; // Uses YouTube's native button class

    // // Add an SVG icon for the button
    const svg1xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.552 15.853q.714-.731.659-1.687t-.824-1.575a51 51 0 0 0-4.504-3.235A164 164 0 0 1 4.296 6.32a338 338 0 0 1 2.939 4.71 98 98 0 0 0 3.076 4.712q.55.815 1.538.83.99.014 1.703-.718M4.46 21q-.605 0-1.113-.267a2 2 0 0 1-.81-.802q-.77-1.35-1.153-2.826A12 12 0 0 1 1 14.08q.028-1.575.508-3.065.48-1.491 1.332-2.841l1.319 2.137q-.468.9-.715 1.87a8 8 0 0 0-.247 1.955q0 1.238.316 2.405t.948 2.208h15.133a9.4 9.4 0 0 0 .893-2.152q.316-1.139.316-2.348 0-3.74-2.568-6.37t-6.221-2.63q-1.017 0-1.991.253a9 9 0 0 0-1.854.703L6.08 4.856q1.318-.9 2.815-1.378A10.2 10.2 0 0 1 12.014 3q2.28 0 4.27.886 1.991.886 3.489 2.419a11.5 11.5 0 0 1 2.362 3.572Q23 11.916 23 14.25q0 1.518-.384 2.953a12 12 0 0 1-1.1 2.728 1.96 1.96 0 0 1-.823.802 2.4 2.4 0 0 1-1.099.267z" fill="#fff" /></svg>`;
    const svg2xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.416 21q-.606 0-1.115-.267a2 2 0 0 1-.812-.802 11.4 11.4 0 0 1-1.13-2.728 10.6 10.6 0 0 1-.357-2.953q0-2.644 1.142-4.992a11.1 11.1 0 0 1 3.208-3.952l.578 2.447a8.6 8.6 0 0 0-2.024 2.953 9.1 9.1 0 0 0-.702 3.544q0 1.181.303 2.334t.909 2.166h15.17q.579-1.04.895-2.18t.316-2.32q0-3.768-2.56-6.384t-6.25-2.616q-.275 0-.537.014-.261.015-.537.07L9.454 3.31q.634-.14 1.267-.225a10.53 10.53 0 0 1 5.561.802A11.1 11.1 0 0 1 19.78 6.29a11.4 11.4 0 0 1 2.354 3.572Q23 11.915 23 14.25q0 1.518-.385 2.94-.385 1.42-1.102 2.741a2 2 0 0 1-.812.802q-.51.267-1.115.267zm8.48-4.725a2.27 2.27 0 0 0 1.266-1.378q.33-.957-.22-1.772a191 191 0 0 0-3.235-4.584Q9.069 6.29 7.334 4.069a120 120 0 0 0 1.17 5.512 109 109 0 0 0 1.446 5.457q.274.927 1.17 1.28a2.2 2.2 0 0 0 1.776-.043" fill="#fff"/></svg>`;
    const svg3xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.414 21q-.606 0-1.115-.267a2 2 0 0 1-.812-.802 12.8 12.8 0 0 1-1.102-2.742A11.2 11.2 0 0 1 1 14.25q0-2.335.867-4.387A11.4 11.4 0 0 1 4.221 6.29a11.1 11.1 0 0 1 3.497-2.405 10.53 10.53 0 0 1 5.561-.802q.634.086 1.267.225l-1.46 2.025a4 4 0 0 0-.536-.07q-.263-.014-.537-.014-3.69 0-6.25 2.616t-2.56 6.384q0 1.18.316 2.32.317 1.14.895 2.18h15.17a8.3 8.3 0 0 0 .909-2.166 9.2 9.2 0 0 0 .303-2.334 9.1 9.1 0 0 0-.703-3.544 8.6 8.6 0 0 0-2.023-2.953l.578-2.447a11.1 11.1 0 0 1 3.208 3.952 11.3 11.3 0 0 1 1.142 4.992 10.6 10.6 0 0 1-.358 2.953 11.4 11.4 0 0 1-1.129 2.728 2 2 0 0 1-.812.802q-.51.267-1.115.267zm6.69-4.725a2.2 2.2 0 0 0 1.776.042q.896-.35 1.17-1.28a109 109 0 0 0 1.446-5.456q.647-2.727 1.17-5.512a167 167 0 0 0-3.373 4.472q-1.638 2.25-3.235 4.584-.55.816-.22 1.772a2.27 2.27 0 0 0 1.266 1.378" fill="#fff"/></svg>`;
    const svg4xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 16.598q1.044-.014 1.539-.773l6.16-9.45-9.24 6.3q-.742.506-.783 1.547t.618 1.716 1.705.66M12 3q1.62 0 3.12.464t2.819 1.392l-2.09 1.35a8.2 8.2 0 0 0-3.85-.956q-3.657 0-6.228 2.63T3.2 14.25q0 1.181.316 2.334t.894 2.166h15.179a8.3 8.3 0 0 0 .92-2.222 9.8 9.8 0 0 0 .29-2.39q0-1.013-.234-1.97a8.3 8.3 0 0 0-.702-1.855l1.32-2.138q.826 1.322 1.306 2.813.48 1.49.51 3.093.027 1.603-.358 3.066a11.8 11.8 0 0 1-1.128 2.784q-.301.507-.825.788a2.3 2.3 0 0 1-1.1.281H4.41q-.578 0-1.1-.281a2.1 2.1 0 0 1-.825-.788A11.4 11.4 0 0 1 1 14.25q0-2.334.866-4.373a11.5 11.5 0 0 1 2.365-3.572q1.5-1.533 3.506-2.42A10.4 10.4 0 0 1 11.999 3" fill="#fff"/></svg>`;

    const getSpeedConfig = (speed) => {
      if (speed < 2.0) {
        return { icon: svg1xSpeedString, nextSpeed: 2.0 };
      }
      if (speed < 3.0) {
        return { icon: svg2xSpeedString, nextSpeed: 3.0 };
      }
      if (speed < 4.0) {
        return { icon: svg3xSpeedString, nextSpeed: 4.0 };
      }
      return { icon: svg4xSpeedString, nextSpeed: 1.0 };
    };

    const updateButtonState = () => {
      const currentVideo = getCurrentVideo();
      if (!currentVideo) return;

      const { icon, nextSpeed } = getSpeedConfig(currentVideo.playbackRate);
      const tooltip = `Play at ${nextSpeed.toFixed(1)}x speed`;
      btn.setAttribute("title", tooltip);
      btn.setAttribute("data-tooltip-title", tooltip);
      btn.setAttribute("data-title-no-tooltip", tooltip);
      btn.ariaLabel = tooltip;
      btn.innerHTML = trustedPolicy ? trustedPolicy.createHTML(icon) : icon;
    };

    updateButtonState();

    // The core logic
    btn.addEventListener("click", () => {
      const currentVideo = getCurrentVideo();
      if (currentVideo) {
        const { nextSpeed } = getSpeedConfig(currentVideo.playbackRate);
        console.log(
          `Changing speed from ${currentVideo.playbackRate} to ${nextSpeed}`,
        );
        currentVideo.playbackRate = nextSpeed;
        updateButtonState();
        setTimeout(updateButtonState, 150);
      }
    });

    const listenerController = new AbortController();
    document.addEventListener("ratechange", updateButtonState, {
      capture: true,
      signal: listenerController.signal,
    });
    document.addEventListener("loadedmetadata", updateButtonState, {
      capture: true,
      signal: listenerController.signal,
    });

    const cleanupObserver = new MutationObserver(() => {
      if (!document.body.contains(btn)) {
        listenerController.abort();
        cleanupObserver.disconnect();
      }
    });
    cleanupObserver.observe(document.body, { childList: true, subtree: true });

    // Insert the button at the beginning of the right controls
    rightControlsLeft.append(btn);
  }

  // Run once on initial page load
  injectSpeedButton();

  // Run every time YouTube finishes an in-page navigation
  window.addEventListener("yt-navigate-finish", () => {
    // Add a slight delay to ensure the video player DOM has caught up with the data layer
    setTimeout(injectSpeedButton, 500);
  });
})();