Greasy Fork is available in English.
Adds an immersive playback speed control interface to YouTube's bottom controls to extend the speed options, while also hiding the default one. Includes keyboard shortcuts.
// ==UserScript==
// @name YouTube Playback Speed Control++
// @namespace https://naeembolchhi.github.io/
// @version 0.6
// @description Adds an immersive playback speed control interface to YouTube's bottom controls to extend the speed options, while also hiding the default one. Includes keyboard shortcuts.
// @author NaeemBolchhi
// @license GPL-3.0-or-later
// @match https://www.youtube.com/*
// @icon data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 29 29"><path d="M14.48 24.5s9.08 0 11.34-.6a3.632 3.632 0 0 0 2.56-2.53c.62-2.22.62-6.89.62-6.89s0-4.64-.62-6.84a3.546 3.546 0 0 0-2.56-2.53c-2.25-.61-11.34-.61-11.34-.61s-9.06 0-11.31.61A3.658 3.658 0 0 0 .59 7.64c-.6 2.2-.6 6.84-.6 6.84s0 4.67.6 6.89a3.731 3.731 0 0 0 2.58 2.53c2.24.6 11.31.6 11.31.6Z" fill="%23f03"/><path d="m19 14.5-7.5-4.25v8.5L19 14.5Z" fill="%23fff"/></svg>
// @run-at document-body
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
// Available speed options
const speeds = [
[0.25, 0.5, 0.75, 1],
[1.25, 1.5, 1.75, 2],
[2.25, 2.5, 2.75, 3],
[3.25, 3.5, 3.75, 4]
];
// Define styles
const speedCSS = `
.ytp-speed-option, .ytp-speed-increase, .ytp-speed-decrease, .ytp-speed-insert {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.ytp-tooltip.ytp-bottom {
top: unset !important;
bottom: 70px;
}
.ytp-internal-speed, .ytp-speed-container.hidden {
display: none !important;
}
.ytp-speed-container.invisible {
opacity: 0;
}
.ytp-speed-container, .ytp-speed-row, .ytp-speed-option, .ytp-speed-display, .ytp-speed-increase, .ytp-speed-decrease {
display: flex;
align-items: center;
justify-content: center;
}
.ytp-speed-container {
opacity: 1;
flex-direction: column;
position: absolute;
right: 0;
bottom: 62px;
z-index: 9999;
background: var(--yt-sys-color-baseline--overlay-background-medium,rgba(0,0,0,.6));
backdrop-filter: var(--yt-frosted-glass-backdrop-filter-override,blur(16px));
color: rgb(255, 255, 255);
border-radius: 12px;
padding: 24px 16px 16px;
font-size: 14px;
gap: 8px;
transition: opacity .1s cubic-bezier(0,0,.2,1);
}
.ytp-speed-row {
gap: 8px;
}
.ytp-speed-option {
cursor: pointer;
height: 32px;
width: 53px;
border-radius: 16px;
background: rgba(255,255,255,.1);
}
.ytp-speed-display {
font-size: 18px;
font-weight: 900;
margin-bottom: 24px;
width: 80%;
text-align: center;
}
.ytp-speed-insert {
flex: 1 0 auto;
}
.ytp-speed-insert::after,
.ytp-speed-option::after {
content: 'x';
}
.ytp-speed-increase, .ytp-speed-decrease {
all: unset;
cursor: pointer;
font-size: 24px;
height: 32px;
width: 32px;
border-radius: 100%;
background: rgba(255,255,255,.1);
}
.ytp-speed-option:hover,.ytp-speed-increase:hover, .ytp-speed-decrease:hover {
background: rgba(255,255,255,.2);
}
.ytp-speed-button {
overflow: visible !important;
position: relative !important;
display: flex !important;
justify-content: center;
}
.ytp-speed-button svg {
transition: transform .1s cubic-bezier(.4,0,1,1);
}
.ytp-speed-button.opened svg {
transform: rotate(30deg);
}
.ytp-tooltip-text {
font-size: 13px;
width: 92px;
text-align: center;
}
.ytp-speed-tooltip {
position: absolute;
bottom: 62px !important;
display: none;
}
.ytp-speed-tooltip.hover {
display: unset;
}
.ytp-right-controls-left:has(>button[aria-expanded="true"]) .ytp-speed-tooltip,
.ytp-speed-button.opened .ytp-speed-tooltip {
display: none !important;
}
.ytp-chrome-bottom:has(.ytp-speed-button.opened) {
opacity: 1 !important;
}
div:has(>.ytp-bezel-text-wrapper)[class*="visible-"] {
display: unset !important;
}
div:has(>.ytp-bezel-text-wrapper)[class*="visible-"] .ytp-bezel-text-wrapper {
opacity: 1 !important;
}
div:has(>.ytp-bezel-text-wrapper)[class*="visible-"] .ytp-bezel-text-wrapper+* {
display: none !important;
}
#movie_player:not(:has(.ytp-speed-container.invisible)) .ytp-fullscreen-quick-actions {
display: none !important;
}
`;
// Create speed control
function createSpeedOptions() {
// Click hold variable for '+' and '-'
let holdTimeout, holdInterval;
const stopHolds = () => {
clearInterval(holdInterval);
clearTimeout(holdTimeout);
};
const speedContainer = document.createElement('div');
speedContainer.classList.add('ytp-speed-container','hidden','invisible');
// Speed display and buttons
const speedDisplay = document.createElement('div');
speedDisplay.classList.add('ytp-speed-display');
const speedDisplaySpan = document.createElement('span');
speedDisplaySpan.classList.add('ytp-speed-insert');
const speedDisplayDecrease = document.createElement('button');
speedDisplayDecrease.classList.add('ytp-speed-decrease');
speedDisplayDecrease.title = 'Decrease playback speed\nCtrl + , (Comma)';
speedDisplayDecrease.addEventListener('mousedown', () => {
changeSpeed('-');
holdTimeout = setTimeout(() => {
holdInterval = setInterval(() => {changeSpeed('-')}, 70);
}, 500);
});
speedDisplayDecrease.addEventListener('mouseup', stopHolds);
speedDisplayDecrease.addEventListener('mouseleave', stopHolds);
const speedDisplayIncrease = document.createElement('button');
speedDisplayIncrease.classList.add('ytp-speed-increase');
speedDisplayIncrease.title = 'Increase playback speed\nCtrl + . (Period)';
speedDisplayIncrease.addEventListener('mousedown', () => {
changeSpeed('+');
holdTimeout = setTimeout(() => {
holdInterval = setInterval(() => {changeSpeed('+')}, 70);
}, 500);
});
speedDisplayIncrease.addEventListener('mouseup', stopHolds);
speedDisplayIncrease.addEventListener('mouseleave', stopHolds);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === ',') {
e.preventDefault();
changeSpeed('-');
} else if (e.ctrlKey && e.key === '.') {
changeSpeed('+');
}
});
const speedDisplayDecreaseSpan = document.createElement('span');
speedDisplayDecreaseSpan.textContent = '–';
const speedDisplayIncreaseSpan = document.createElement('span');
speedDisplayIncreaseSpan.textContent = '+';
speedDisplayDecrease.appendChild(speedDisplayDecreaseSpan);
speedDisplayIncrease.appendChild(speedDisplayIncreaseSpan);
speedDisplay.appendChild(speedDisplayDecrease);
speedDisplay.appendChild(speedDisplaySpan);
speedDisplay.appendChild(speedDisplayIncrease);
speedContainer.appendChild(speedDisplay);
// Add speed options
for (let x = 0; x < speeds.length; x++) {
const speedRow = document.createElement('div');
speedRow.classList.add('ytp-speed-row');
for (let y = 0; y < speeds[x].length; y++) {
const speedOption = document.createElement('span');
speedOption.classList.add('ytp-speed-option');
speedOption.textContent = speeds[x][y];
speedOption.addEventListener('click', () => {
const video = document.querySelector('video');
if (video) {
// video.playbackRate = speeds[x][y];
setSpeedValue(speedOption.textContent);
}
});
speedRow.appendChild(speedOption);
}
speedContainer.appendChild(speedRow);
}
return speedContainer;
}
// Create speed button in chromeBottom
function createSpeedButton() {
const settingsButton = document.querySelector('.ytp-chrome-bottom .ytp-settings-button');
const speedContainer = document.querySelector('.ytp-speed-container');
const speedButton = document.createElement('button');
speedButton.classList.add('ytp-button', 'ytp-speed-button');
speedButton.appendChild(settingsButton.children[0].cloneNode(true));
speedButton.querySelector('path').setAttribute('d', "M12 1c1.44 0 2.87.28 4.21.83a11 11 0 0 1 3.45 2.27l-1.81 1.05c-3.78-3.23-9.46-2.79-12.69.99A8.986 8.986 0 0 0 3 12a9 9 0 0 0 18 0v-.44c-.03-.4-.08-.8-.15-1.2l1.81-1.05c1.49 5.89-2.08 11.87-7.98 13.36-1.36.34-2.78.42-4.17.23-6.02-.82-10.24-6.36-9.42-12.38C1.83 5.06 6.49.99 12 1Zm7.08 6.25-7.96 3.25a1.75 1.75 0 0 0-.92 2.28c.38.88 1.4 1.29 2.28.92.13-.06.25-.13.36-.21l6.8-5.26c.25-.19.29-.55.1-.8a.579.579 0 0 0-.66-.18h-.01Z");
const speedButtonTooltip = document.createElement('div');
speedButtonTooltip.classList.add('ytp-tooltip','ytp-bottom','ytp-speed-tooltip');
const speedButtonTooltip2 = document.createElement('div');
speedButtonTooltip2.classList.add('ytp-tooltip-text-wrapper');
const speedButtonTooltip3 = document.createElement('div');
speedButtonTooltip3.classList.add('ytp-tooltip-bottom-text');
const speedButtonTooltip4 = document.createElement('span');
speedButtonTooltip4.classList.add('ytp-tooltip-text');
speedButtonTooltip4.textContent = 'Playback speed';
speedButtonTooltip3.appendChild(speedButtonTooltip4);
speedButtonTooltip2.appendChild(speedButtonTooltip3);
speedButtonTooltip.appendChild(speedButtonTooltip2);
speedButton.appendChild(speedButtonTooltip);
speedButton.addEventListener('click', () => {
if (speedContainer.classList.contains('hidden')) {
speedContainer.classList.remove('hidden');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
speedContainer.classList.remove('invisible');
setTimeout(() => {
speedButton.classList.add('opened');
}, 100);
});
});
} else {
speedContainer.classList.add('invisible');
speedButton.classList.remove('opened');
setTimeout(() => {
speedContainer.classList.add('hidden');
}, 100);
}
});
document.body.addEventListener('click', (e) => {
if (e.target.closest('.ytp-speed-button.ytp-button') || e.target.closest('.ytp-speed-container') || speedContainer.classList.contains('hidden')) {return;}
speedContainer.classList.add('invisible');
speedButton.classList.remove('opened');
setTimeout(() => {
speedContainer.classList.add('hidden');
}, 100);
});
function hoverState() {
speedButtonTooltip.classList.add('hover');
}
function hoverStateGone() {
speedButtonTooltip.classList.remove('hover');
}
speedButton.addEventListener('mouseenter', () => {
hoverState();
});
speedButton.addEventListener('mouseleave', () => {
hoverStateGone();
});
speedButton.addEventListener('focus', () => {
hoverState();
});
speedButton.addEventListener('blur', () => {
hoverStateGone();
});
settingsButton.parentNode.insertBefore(speedButton, settingsButton);
}
// Deal with it when an option is clicked
function setSpeedValue(speedValue) {
const displayTextSpan = document.querySelector('.ytp-speed-insert');
displayTextSpan.textContent = parseFloat(speedValue).toFixed(2);
const video = document.querySelector('video');
video.playbackRate = parseFloat(speedValue);
bezelSpeedValue(speedValue);
}
// Display speed value in bezel
function bezelSpeedValue(speedValue) {
const bezel = document.querySelector('.ytp-bezel-text-wrapper');
const videoSpeed = parseFloat(speedValue).toFixed(2);
const dateNow = parseInt(Date.now()).toString();
bezel.children[0].textContent = videoSpeed + 'x';
bezel.parentNode.classList.add('visible-' + dateNow);
setTimeout(() => {
bezel.parentNode.classList.remove('visible-' + dateNow)
}, 1000);
}
// Insert speed options into the player
function insertSpeedOptions() {
const chromeBottom = document.querySelector('.ytp-chrome-bottom');
if (!chromeBottom || document.querySelector('.ytp-speed-container')) {return;}
// Safely add styles
GM_addStyle(speedCSS);
// Add speed menu
chromeBottom.appendChild(createSpeedOptions());
// Add speed button
createSpeedButton();
// Set initial highlight
setInitialSpeed();
}
// Initialize highlight speed options
function setInitialSpeed() {
const currentSpeed = document.querySelector('video')?.playbackRate || 1;
setSpeedValue(currentSpeed);
}
// Hide internal speed settings
function hideInternalSpeed() {
const chromeBottom = document.querySelector('.ytp-chrome-bottom');
if (!chromeBottom || document.querySelector('.ytp-internal-speed')) {return;}
const labels = document.querySelectorAll('#ytp-id-5 .ytp-menuitem-label');
for (let x = 0; x < labels.length; x++) {
if (labels[x].textContent.match(/playback speed/i)) {
labels[x].closest('.ytp-menuitem').classList.add('ytp-internal-speed');
}
}
}
// Change video speed
function changeSpeed(key) {
const changeRate = 0.05;
const currentSpeed = parseFloat(document.querySelector('video').playbackRate);
let newSpeed;
if (key === '-') {
newSpeed = currentSpeed - changeRate;
} else if (key === '+') {
newSpeed = currentSpeed + changeRate;
}
if (newSpeed < 0.05) {
newSpeed = 0.05;
}
setSpeedValue(newSpeed);
}
// Use MutationObserver to listen for player changes
const observer = new MutationObserver(() => {
insertSpeedOptions();
try {hideInternalSpeed()} catch {}
});
observer.observe(document.body, { childList: true, subtree: true });
// Execute after page loads
window.addEventListener('load', insertSpeedOptions);
})();