YouTube Custom Speed Control Button

Injects a custom speed control button into the YouTube player

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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