HTML5 Video Player Speed Control

Control the playback speed of HTML5 video players with keyboard shortcuts.

// ==UserScript==
// @name         HTML5 Video Player Speed Control
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Control the playback speed of HTML5 video players with keyboard shortcuts.
// @author       JJJ
// @match        *://*/*
// @icon         https://logos-download.com/wp-content/uploads/2017/07/HTML5_logo.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Active video element
    let video = null;
    // Use localStorage to persist playback rate across reloads and videos
    const STORAGE_KEY = 'tm_html5_video_speed';
    let playbackRate = parseFloat(localStorage.getItem(STORAGE_KEY)) || 1.0; // Current playback rate
    let previousPlaybackRate = playbackRate; // Previous rate for toggling

    // Speed indicator overlay setup
    const speedIndicator = document.createElement('div');
    speedIndicator.style.position = 'absolute';
    speedIndicator.style.top = '10px';
    speedIndicator.style.left = '10px';
    speedIndicator.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
    speedIndicator.style.color = '#fff';
    speedIndicator.style.padding = '5px';
    speedIndicator.style.fontFamily = 'Arial, sans-serif';
    speedIndicator.style.fontSize = '12px';
    speedIndicator.style.zIndex = '9999';
    speedIndicator.style.pointerEvents = 'none'; // Let clicks pass through
    speedIndicator.style.userSelect = 'none';    // No text selection
    speedIndicator.style.webkitUserSelect = 'none';
    speedIndicator.style.opacity = '0.6';

    // Show current speed and fade out after 2s
    function updateSpeedIndicator() {
        if (!video) return;
        playbackRate = video.playbackRate;
        // Persist the current playback rate
        localStorage.setItem(STORAGE_KEY, playbackRate);
        speedIndicator.textContent = `Speed: ${playbackRate.toFixed(1)}x`;
        speedIndicator.style.opacity = '1';
        clearTimeout(speedIndicator.hideTimeout);
        speedIndicator.hideTimeout = setTimeout(() => {
            speedIndicator.style.opacity = '0.6';
        }, 2000);
    }

    // Place indicator as fixed in fullscreen, absolute otherwise
    function updateSpeedIndicatorPosition() {
        if (!video) return;
        const isFullscreen = document.fullscreenElement === video ||
            document.webkitFullscreenElement === video ||
            video.webkitDisplayingFullscreen === true ||
            (video.offsetWidth === window.innerWidth && video.offsetHeight === window.innerHeight);
        speedIndicator.style.position = isFullscreen ? 'fixed' : 'absolute';
    }

    // Toggle between a fixed speed and previous speed
    function toggleSpeed(fixedSpeed) {
        if (playbackRate !== fixedSpeed) {
            previousPlaybackRate = playbackRate;
            playbackRate = fixedSpeed;
        } else {
            playbackRate = previousPlaybackRate;
        }
        video.playbackRate = playbackRate;
        updateSpeedIndicator();
    }

    // Increase speed by 0.1
    function speedUpVideo() {
        previousPlaybackRate = playbackRate;
        playbackRate = video.playbackRate + 0.1;
        video.playbackRate = playbackRate;
        updateSpeedIndicator();
    }

    // Decrease speed by 0.1
    function slowDownVideo() {
        previousPlaybackRate = playbackRate;
        playbackRate = video.playbackRate - 0.1;
        video.playbackRate = playbackRate;
        updateSpeedIndicator();
    }

    // Toggle 1.5x speed
    function setFastSpeed() { toggleSpeed(1.5); }
    // Toggle 1.8x speed
    function setFasterSpeed() { toggleSpeed(1.8); }
    // Toggle/reset 1.0x speed
    function resetSpeed() { toggleSpeed(1.0); }

    // Show or hide the speed indicator
    function toggleSpeedIndicator() {
        speedIndicator.style.display = speedIndicator.style.display === 'none' ? 'block' : 'none';
    }

    // Attach indicator and listeners to a video element
    function setupVideo(v) {
        if (video === v) return;
        video = v;
        // Always use the persisted playbackRate for new videos
        playbackRate = parseFloat(localStorage.getItem(STORAGE_KEY)) || 1.0;
        previousPlaybackRate = playbackRate;
        const container = video.parentElement;
        if (container && !container.contains(speedIndicator)) {
            container.style.position = 'relative';
            container.appendChild(speedIndicator);
        }
        updateSpeedIndicator();
        updateSpeedIndicatorPosition();
        video.addEventListener('ratechange', updateSpeedIndicator);
        window.addEventListener('resize', updateSpeedIndicatorPosition);

        // Helper to force playback rate and update indicator
        function forcePlaybackRate() {
            video.playbackRate = playbackRate;
            updateSpeedIndicator();
        }

        // Always enforce playback rate on these events
        video.addEventListener('canplay', forcePlaybackRate);
        video.addEventListener('play', forcePlaybackRate);
        video.addEventListener('playing', forcePlaybackRate);
        video.addEventListener('loadedmetadata', forcePlaybackRate);

        // Set initial playback rate
        if (video.readyState < 3) {
            video.addEventListener('canplay', forcePlaybackRate, { once: true });
        } else {
            forcePlaybackRate();
        }
    }

    // Watch for new video elements in the DOM
    const observer = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.tagName === 'VIDEO') {
                        setupVideo(node);
                    } else {
                        const vid = node.querySelector && node.querySelector('video');
                        if (vid) setupVideo(vid);
                    }
                }
            });
        });
    });
    // Start observing for video elements in the DOM
    observer.observe(document.body, { childList: true, subtree: true });

    // Attach to first video on page load
    window.addEventListener('load', () => {
        const vid = document.querySelector('video');
        if (vid) setupVideo(vid);
    });

    // Keyboard shortcuts for speed and indicator
    document.addEventListener('keydown', (event) => {
        // Ignore if typing in input, textarea, or contenteditable
        const tag = event.target.tagName;
        if (tag === 'INPUT' || tag === 'TEXTAREA' || event.target.isContentEditable) return;
        const key = event.key.toLowerCase();
        switch (key) {
            case 'd': speedUpVideo(); break;      // +0.1x
            case 's': slowDownVideo(); break;     // -0.1x
            case 'g': setFastSpeed(); break;      // 1.5x toggle
            case 'h': setFasterSpeed(); break;    // 1.8x toggle
            case 'r': resetSpeed(); break;        // 1.0x toggle/reset
            case 'v': toggleSpeedIndicator(); break; // Show/hide
        }
    });
})();