NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name YouTube - Playback Speed Slider // @name:fr YouTube - Curseur de vitesse de lecture // @name:es YouTube - Deslizador de velocidad de reproducción // @name:de YouTube - Wiedergabegeschwindigkeit-Schieberegler // @name:it YouTube - Cursore della velocità di riproduzione // @name:zh-CN YouTube - 播放速度滑块 // @namespace https://gist.github.com/4lrick/8149bd289cf94889a97aae9732a17144 // @version 1.0 // @description Adds a slider for playback speed control on YouTube videos. // @description:fr Ajoute un curseur pour contrôler la vitesse de lecture des vidéos YouTube. // @description:es Agrega un deslizador para controlar la velocidad de reproducción en videos de YouTube. // @description:de Fügt einen Schieberegler zur Steuerung der Wiedergabegeschwindigkeit in YouTube-Videos hinzu. // @description:it Aggiunge un cursore per controllare la velocità di riproduzione dei video di YouTube. // @description:zh-CN 在YouTube视频中添加播放速度控制滑块。 // @author 4lrick // @match https://www.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license GPL-3.0-only // ==/UserScript== (function () { 'use strict'; const CONSTANTS = { MIN_SPEED: 0.25, MAX_SPEED: 10.0, DEFAULT_SPEED: 1.0, DEFAULT_SPEED_STEP: 0.05, }; const STORAGE_KEY = 'yt-player-speed'; const REMEMBER_SPEED_KEY = 'rememberSpeed'; const SPEED_STEP_KEY = 'speedStep'; let speedStep = GM_getValue(SPEED_STEP_KEY, CONSTANTS.DEFAULT_SPEED_STEP); let rememberSpeed = GM_getValue(REMEMBER_SPEED_KEY, true); let savedSpeed = parseFloat(localStorage.getItem(STORAGE_KEY)) || CONSTANTS.DEFAULT_SPEED; let rememberSpeedMenuId, speedStepMenuId; let sliderRef = null; let observer = null; function updateRememberSpeedMenu() { if (rememberSpeedMenuId) { GM_unregisterMenuCommand(rememberSpeedMenuId); } const status = rememberSpeed ? 'ON' : 'OFF'; rememberSpeedMenuId = GM_registerMenuCommand(`Remember speed (current: ${status})`, toggleRememberSpeed); } function updateSpeedStepMenu() { if (speedStepMenuId) { GM_unregisterMenuCommand(speedStepMenuId); } speedStepMenuId = GM_registerMenuCommand(`Set speed step (current: ${speedStep})`, setSpeedStep); } function toggleRememberSpeed() { const video = document.querySelector('video'); if (!video) return; rememberSpeed = !rememberSpeed; if (rememberSpeed) { const currentSpeed = video.playbackRate; localStorage.setItem(STORAGE_KEY, currentSpeed); savedSpeed = currentSpeed; } GM_setValue(REMEMBER_SPEED_KEY, rememberSpeed); updateRememberSpeedMenu(); } function setSpeedStep() { const input = prompt('Enter a new speed step (e.g., 0.25):', speedStep); const newSpeedStep = parseFloat(input); if (!isNaN(newSpeedStep) && newSpeedStep > 0) { speedStep = newSpeedStep; GM_setValue(SPEED_STEP_KEY, speedStep); updateSpeedStepMenu(); if (sliderRef) { sliderRef.step = String(speedStep); } } else { alert('Invalid input. Please enter a positive number.'); } } function applyRememberedSpeed(video) { if (rememberSpeed && video) { video.playbackRate = parseFloat(savedSpeed); } } function initMenu() { const menuButton = document.querySelector('.ytp-settings-button'); const menu = document.querySelector('.ytp-settings-menu'); if (menuButton && menu) { menu.style.opacity = '0'; menuButton.click(); menuButton.click(); menu.style.opacity = '1'; } } function formatSpeed(value) { const rounded = Math.round(value * 100) / 100; return rounded % 1 === 0 ? `${rounded.toFixed(0)}x` : `${rounded}x`; } function handleSlider(slider, video, speedValue) { const inputListener = () => { const newSpeed = parseFloat(slider.value); video.playbackRate = newSpeed; speedValue.textContent = formatSpeed(newSpeed); if (rememberSpeed) { savedSpeed = newSpeed; localStorage.setItem(STORAGE_KEY, newSpeed); } slider.style.setProperty('--yt-slider-shape-gradient-percent', `${(newSpeed / slider.max) * 100}%`); }; const wheelListener = (e) => { e.preventDefault(); const delta = e.deltaY < 0 ? speedStep : -speedStep; let newSpeed = parseFloat(slider.value) + delta; newSpeed = Math.max(parseFloat(slider.min), Math.min(parseFloat(slider.max), newSpeed)); slider.value = newSpeed; video.playbackRate = newSpeed; speedValue.textContent = formatSpeed(newSpeed); if (rememberSpeed) { savedSpeed = newSpeed; localStorage.setItem(STORAGE_KEY, newSpeed); } slider.style.setProperty('--yt-slider-shape-gradient-percent', `${(newSpeed / slider.max) * 100}%`); }; slider.addEventListener('input', inputListener); slider.addEventListener('wheel', wheelListener); } function createSlider(playbackSpeedContent, video) { playbackSpeedContent.textContent = ''; const container = document.createElement('div'); container.style.display = 'flex'; container.style.alignItems = 'center'; const speedValue = document.createElement('span'); speedValue.textContent = formatSpeed(video.playbackRate); speedValue.style.width = '50px'; const slider = document.createElement('input'); slider.type = 'range'; slider.min = String(CONSTANTS.MIN_SPEED); slider.max = String(CONSTANTS.MAX_SPEED); slider.step = String(speedStep); slider.value = video.playbackRate; slider.classList.add('ytp-input-slider'); slider.style.margin = '0 10px'; slider.style.setProperty('--yt-slider-shape-gradient-percent', `${(video.playbackRate / slider.max) * 100}%`); slider.addEventListener('click', (e) => e.stopPropagation()); handleSlider(slider, video, speedValue); container.appendChild(speedValue); container.appendChild(slider); playbackSpeedContent.appendChild(container); sliderRef = slider; } function findPlaybackSpeedMenuItem() { const menuItem = document.querySelector('.ytp-menuitem[aria-label*="Playback speed"]'); if (menuItem) return menuItem; const speedIcon = document.querySelector('.ytp-menuitem-icon svg path[d*="M10,8v8l6-4L10,8"]'); return speedIcon ? speedIcon.closest('.ytp-menuitem') : null; } function setupSliderMenuItem() { const video = document.querySelector('video'); if (!video) return; applyRememberedSpeed(video); const playbackSpeedMenuItem = findPlaybackSpeedMenuItem(); if (!playbackSpeedMenuItem) return; const playbackSpeedContent = playbackSpeedMenuItem.querySelector('.ytp-menuitem-content'); if (playbackSpeedContent && !playbackSpeedContent.querySelector('input[type="range"]')) { createSlider(playbackSpeedContent, video); initMenu(); } } function observeVideoChanges() { if (observer) { observer.disconnect(); } observer = new MutationObserver(setupSliderMenuItem); observer.observe(document.body, { childList: true, subtree: true }); } updateSpeedStepMenu(); updateRememberSpeedMenu(); observeVideoChanges(); })();