Adds a convenient overlay to control YouTube video playback speed with keyboard shortcuts and perfect sync with native controls
// ==UserScript==
// @name YouTube Playback Speed Control Overlay
// @namespace https://greasyfork.org/en/users/1317369-bishoy-asaad
// @version 1.0.0
// @description Adds a convenient overlay to control YouTube video playback speed with keyboard shortcuts and perfect sync with native controls
// @author Bishoy
// @license MIT
// @match https://www.youtube.com/watch*
// @match https://youtube.com/watch*
// @match https://www.youtube.com/shorts*
// @match https://youtube.com/shorts*
// @icon https://www.youtube.com/favicon.ico
// @homepage https://greasyfork.org/en/scripts/533340-youtube-playback-speed-control-overlay
// @grant none
// @compatible chrome
// @compatible firefox
// @compatible edge
// @compatible safari
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// Configuration
const speedStep = 0.25;
const minSpeed = 0.25;
const maxSpeed = 3.0;
const updateInterval = 1000;
// Create and style the overlay
function createSpeedControl() {
const container = document.createElement('div');
container.id = 'yt-speed-control';
container.style.cssText = `
position: absolute;
bottom: 80px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
z-index: 9999;
display: flex;
align-items: center;
font-family: Arial, sans-serif;
user-select: none;
opacity: 0;
transition: opacity 0.3s;
pointer-events: auto;
`;
const decreaseBtn = document.createElement('button');
decreaseBtn.textContent = '−';
decreaseBtn.style.cssText = `
background-color: #333;
color: white;
border: none;
border-radius: 3px;
width: 28px;
height: 28px;
font-size: 16px;
cursor: pointer;
margin-right: 8px;
`;
decreaseBtn.addEventListener('click', () => changeSpeed('decrease'));
const speedDisplay = document.createElement('div');
speedDisplay.id = 'yt-speed-display';
speedDisplay.textContent = `1.00x`;
speedDisplay.style.cssText = `
font-size: 14px;
font-weight: bold;
margin: 0 8px;
min-width: 46px;
text-align: center;
`;
const increaseBtn = document.createElement('button');
increaseBtn.textContent = '+';
increaseBtn.style.cssText = `
background-color: #333;
color: white;
border: none;
border-radius: 3px;
width: 28px;
height: 28px;
font-size: 16px;
cursor: pointer;
margin-left: 8px;
`;
increaseBtn.addEventListener('click', () => changeSpeed('increase'));
container.appendChild(decreaseBtn);
container.appendChild(speedDisplay);
container.appendChild(increaseBtn);
return container;
}
// Update YouTube's internal speed state
function updateYouTubeSpeedState(speed) {
try {
// Update session storage
const storageData = {
data: speed.toString(),
creation: Date.now()
};
sessionStorage.setItem('yt-player-playback-rate', JSON.stringify(storageData));
// Find the YouTube settings menu and update its state
const menuItems = document.querySelectorAll('div.ytp-menuitem');
menuItems.forEach(item => {
if (item.querySelector('.ytp-menuitem-label')?.textContent?.includes('Playback speed')) {
const valueElement = item.querySelector('.ytp-menuitem-content');
if (valueElement) {
valueElement.textContent = speed === 1 ? 'Normal' : speed + '×';
}
// Update the checked state
const checkmark = item.querySelector('.ytp-menuitem-toggle-checkbox');
if (checkmark) {
const currentSpeedText = item.querySelector('.ytp-menuitem-content')?.textContent;
const isSelected = currentSpeedText === (speed === 1 ? 'Normal' : speed + '×');
checkmark.style.display = isSelected ? '' : 'none';
}
}
});
// Dispatch a ratechange event to trigger YouTube's internal handlers
const video = document.querySelector('video');
if (video) {
video.dispatchEvent(new Event('ratechange'));
}
} catch (e) {
console.log('Error updating YouTube speed state:', e);
}
}
// Change playback speed
function changeSpeed(direction) {
const video = document.querySelector('video');
if (!video) return;
let currentSpeed = video.playbackRate;
let newSpeed;
if (direction === 'increase') {
newSpeed = Math.min(maxSpeed, currentSpeed + speedStep);
} else {
newSpeed = Math.max(minSpeed, currentSpeed - speedStep);
}
// Try to use YouTube's native API first
if (window.yt && window.yt.player && window.yt.player.getPlayerByElement) {
const playerElement = document.querySelector('#movie_player');
if (playerElement) {
const player = window.yt.player.getPlayerByElement(playerElement);
if (player && player.setPlaybackRate) {
player.setPlaybackRate(newSpeed);
updateSpeedDisplay(newSpeed);
updateYouTubeSpeedState(newSpeed);
return;
}
}
}
// Fallback
setPlaybackRate(newSpeed);
}
// Fallback method to set speed directly
function setPlaybackRate(speed) {
const video = document.querySelector('video');
if (video) {
video.playbackRate = speed;
updateSpeedDisplay(speed);
updateYouTubeSpeedState(speed);
}
}
// Update the speed display text
function updateSpeedDisplay(speed) {
const display = document.getElementById('yt-speed-display');
if (display) {
display.textContent = `${speed.toFixed(2)}x`;
}
}
// Show/hide controls based on YouTube controls visibility
function updateControlsVisibility() {
const speedControl = document.getElementById('yt-speed-control');
if (!speedControl) return;
const playerContainer = document.querySelector('#movie_player');
if (!playerContainer) return;
const isControlsVisible = playerContainer.classList.contains('ytp-autohide') === false;
if (isControlsVisible) {
speedControl.style.opacity = '0.8';
} else {
speedControl.style.opacity = '0';
}
}
// Sync with YouTube's speed changes
function setupSpeedChangeListener() {
const video = document.querySelector('video');
if (video) {
video.addEventListener('ratechange', () => {
updateSpeedDisplay(video.playbackRate);
updateYouTubeSpeedState(video.playbackRate);
});
}
}
// Initialize control and monitor for player
let controlAdded = false;
let speedControl = null;
function initializeControl() {
const video = document.querySelector('video');
const playerContainer = document.querySelector('#movie_player');
if (video && playerContainer) {
if (!controlAdded) {
speedControl = createSpeedControl();
playerContainer.appendChild(speedControl);
controlAdded = true;
updateSpeedDisplay(video.playbackRate);
setupSpeedChangeListener();
document.addEventListener('keydown', function(e) {
if (document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA') {
if (e.key === ']') changeSpeed('increase');
else if (e.key === '[') changeSpeed('decrease');
}
});
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' &&
mutation.attributeName === 'class') {
updateControlsVisibility();
}
});
});
observer.observe(playerContainer, { attributes: true });
playerContainer.addEventListener('mousemove', updateControlsVisibility);
updateControlsVisibility();
} else {
const isFullscreen = document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement;
if (speedControl) {
speedControl.style.bottom = isFullscreen ? '120px' : '80px';
}
updateSpeedDisplay(video.playbackRate);
updateControlsVisibility();
}
} else if (controlAdded && speedControl && !playerContainer) {
speedControl.remove();
controlAdded = false;
}
}
setInterval(initializeControl, updateInterval);
})();