Greasy Fork is available in English.

Simple H5Player Controller

Control HTML5 video players: custom hotkeys, save progress, sync settings globally with smart speed memory.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Simple H5Player Controller
// @namespace    https://github.com/xyseer
// @version      1.2
// @description  Control HTML5 video players: custom hotkeys, save progress, sync settings globally with smart speed memory.
// @author       xyseer
// @license      MIT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION & SAVED DATA ---
    let enableResumeProgress = GM_getValue('ENABLE_RESUME_PROGRESS', true);
    let enableGlobalSpeed = GM_getValue('ENABLE_GLOBAL_SPEED', true);
    let enableGlobalVolume = GM_getValue('ENABLE_GLOBAL_VOLUME', true);
    let enableAutoPlay = GM_getValue('ENABLE_AUTOPLAY', true);

    let currentFPS = GM_getValue('H5PLAYER_FPS', 30);
    let globalPlaybackSpeed = GM_getValue('H5PLAYER_GLOBAL_SPEED', 1.0);
    let globalVolume = GM_getValue('H5PLAYER_GLOBAL_VOLUME', 1.0);

    // Persistent memory for the last custom speed used (defaults to 2.0 if never set)
    let lastModifiedSpeed = GM_getValue('H5PLAYER_LAST_MODIFIED_SPEED', 2.0);
    // ----------------------------------

    // --- EXTENSION MENU OPTIONS ---
    GM_registerMenuCommand(`Toggle Auto-Play (Currently: ${enableAutoPlay ? "ON" : "OFF"})`, () => {
        enableAutoPlay = !enableAutoPlay;
        GM_setValue('ENABLE_AUTOPLAY', enableAutoPlay);
        alert(`"Auto-Play" is now ${enableAutoPlay ? "ON" : "OFF"}.`);
    });

    GM_registerMenuCommand(`Toggle Global Volume Sync (Currently: ${enableGlobalVolume ? "ON" : "OFF"})`, () => {
        enableGlobalVolume = !enableGlobalVolume;
        GM_setValue('ENABLE_GLOBAL_VOLUME', enableGlobalVolume);
        alert(`"Global Volume Sync" is now ${enableGlobalVolume ? "ON" : "OFF"}.`);
    });

    GM_registerMenuCommand(`Toggle Global Speed Sync (Currently: ${enableGlobalSpeed ? "ON" : "OFF"})`, () => {
        enableGlobalSpeed = !enableGlobalSpeed;
        GM_setValue('ENABLE_GLOBAL_SPEED', enableGlobalSpeed);
        alert(`"Global Speed Sync" is now ${enableGlobalSpeed ? "ON" : "OFF"}.`);
    });

    GM_registerMenuCommand(`Toggle Restore Progress (Currently: ${enableResumeProgress ? "ON" : "OFF"})`, () => {
        enableResumeProgress = !enableResumeProgress;
        GM_setValue('ENABLE_RESUME_PROGRESS', enableResumeProgress);
        alert(`"Restore to last progress" is now ${enableResumeProgress ? "ON" : "OFF"}.`);
    });

    GM_registerMenuCommand(`Set Frame Rate for D/F Keys (Currently: ${currentFPS} FPS)`, () => {
        const userInput = prompt("Enter the FPS of the video for frame-seeking (e.g., 24, 30, 60):", currentFPS);
        const parsedFPS = parseInt(userInput, 10);

        if (!isNaN(parsedFPS) && parsedFPS > 0) {
            currentFPS = parsedFPS;
            GM_setValue('H5PLAYER_FPS', currentFPS);
            alert(`FPS set to ${currentFPS}.`);
        }
    });
    // -----------------------------

    // --- PERFORMANCE / MEMORY STATE ---
    let toastTimeout = null;
    let saveSpeedTimeout = null;
    let saveVolumeTimeout = null;
    let cachedVideo = null;

    function isTyping(e) {
        const target = e.target;
        return ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) || target.isContentEditable;
    }

    function getVideo() {
        if (cachedVideo && document.contains(cachedVideo)) {
            return cachedVideo;
        }

        const videos = document.querySelectorAll('video');
        let bestVideo = null;
        let maxArea = 0;
        videos.forEach(v => {
            const rect = v.getBoundingClientRect();
            const area = rect.width * rect.height;
            if (area > maxArea) {
                maxArea = area;
                bestVideo = v;
            }
        });

        cachedVideo = bestVideo;
        return bestVideo;
    }

    function showToast(video, message) {
        // Find optimal container context so toast remains visible during native fullscreen modes
        let container = document.fullscreenElement || video.parentElement || document.body;
        let toast = document.getElementById('h5player-toast');
        
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'h5player-toast';
            toast.style.cssText = `
                position: absolute;
                top: 10px;
                left: 10px;
                background: rgba(0, 0, 0, 0.7);
                color: white;
                padding: 5px 10px;
                border-radius: 4px;
                font-family: sans-serif;
                font-size: 14px;
                z-index: 2147483647;
                pointer-events: none;
                transition: opacity 0.2s;
            `;
        }

        if (container !== document.body && window.getComputedStyle(container).position === 'static') {
            container.style.position = 'relative';
        }

        if (toast.parentElement !== container) {
            container.appendChild(toast);
        }

        if (container === document.body) {
            const rect = video.getBoundingClientRect();
            toast.style.position = 'absolute';
            toast.style.top = `${rect.top + window.scrollY + 10}px`;
            toast.style.left = `${rect.left + window.scrollX + 10}px`;
        } else {
            toast.style.position = 'absolute';
            toast.style.top = '10px';
            toast.style.left = '10px';
        }

        toast.textContent = message;
        toast.style.opacity = '1';

        if (toastTimeout) clearTimeout(toastTimeout);

        toastTimeout = setTimeout(() => {
            if (toast) toast.style.opacity = '0';
        }, 1500);
    }

    // Generates a dynamic unique tracking token mixing both full URL state and internal source stream
    function getUniqueVideoKey(video) {
        const url = window.location.href;
        const src = video.currentSrc || '';
        const combined = url + "_" + src;
        let hash = 0;
        for (let i = 0; i < combined.length; i++) {
            hash = (hash << 5) - hash + combined.charCodeAt(i);
            hash |= 0;
        }
        return `h5_progress_${Math.abs(hash)}`;
    }

    function safeRestoreProgress(video) {
        if (!enableResumeProgress) return;

        const progressKey = getUniqueVideoKey(video);
        
        // Block progress bleeding by ensuring this exact stream/URL instance hasn't been verified yet
        if (video.__h5_last_restored_key === progressKey) return;

        const savedTime = localStorage.getItem(progressKey);
        if (savedTime && parseFloat(savedTime) > 0) {
            const time = parseFloat(savedTime);
            if (Math.abs(video.currentTime - time) > 2) {
                video.currentTime = time;
                showToast(video, `Restored to ${Math.floor(time)}s`);
            }
        }
        video.__h5_last_restored_key = progressKey;
    }

    // Helper: Update global speed with Debounce & Persistent Memory
    function updateSpeed(video, newSpeed) {
        const formattedSpeed = parseFloat(newSpeed.toFixed(1));
        video.playbackRate = formattedSpeed;

        if (enableGlobalSpeed) {
            globalPlaybackSpeed = formattedSpeed;

            // If the user picked a speed other than 1.0, update our "last custom speed" memory
            if (formattedSpeed !== 1.0) {
                lastModifiedSpeed = formattedSpeed;
                GM_setValue('H5PLAYER_LAST_MODIFIED_SPEED', lastModifiedSpeed);
            }

            if (saveSpeedTimeout) clearTimeout(saveSpeedTimeout);
            saveSpeedTimeout = setTimeout(() => {
                GM_setValue('H5PLAYER_GLOBAL_SPEED', globalPlaybackSpeed);
            }, 500);
        }
        showToast(video, `Speed: ${formattedSpeed}x`);
    }

    // Helper: Update global volume with Debounce
    function updateVolume(video, newVolume) {
        video.volume = newVolume;
        if (enableGlobalVolume) {
            globalVolume = newVolume;

            if (saveVolumeTimeout) clearTimeout(saveVolumeTimeout);
            saveVolumeTimeout = setTimeout(() => {
                GM_setValue('H5PLAYER_GLOBAL_VOLUME', globalVolume);
            }, 500);
        }
        showToast(video, `Volume: ${Math.round(video.volume * 100)}%`);
    }

    // Handle Keyboard Shortcuts
    window.addEventListener('keydown', function(e) {
        if (isTyping(e)) return;

        const video = getVideo();
        if (!video) return;

        let handled = true;

        switch(e.key.toLowerCase()) {
            // Fullscreen Toggle
            case 'enter':
                if (!document.fullscreenElement) {
                    const requestFS = video.requestFullscreen || video.webkitRequestFullscreen || video.mozRequestFullScreen || video.msRequestFullscreen;
                    if (requestFS) {
                        requestFS.call(video).catch(() => {
                            if (video.parentElement) {
                                const reqParent = video.parentElement.requestFullscreen || video.parentElement.webkitRequestFullscreen;
                                if (reqParent) reqParent.call(video.parentElement).catch(() => {});
                            }
                        });
                    }
                } else {
                    const exitFS = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen;
                    if (exitFS) exitFS.call(document).catch(() => {});
                }
                break;

            // Play / Pause Toggle
            case ' ':
                if (video.paused) {
                    video.play().catch(() => {});
                    showToast(video, "Play");
                } else {
                    video.pause();
                    showToast(video, "Pause");
                }
                break;

            // Mute / Unmute Toggle
            case 'm':
                video.muted = !video.muted;
                showToast(video, video.muted ? "Muted" : `Volume: ${Math.round(video.volume * 100)}%`);
                break;

            // Speed Control
            case 'c':
                updateSpeed(video, Math.min(16, video.playbackRate + 0.1));
                break;
            case 'x':
                updateSpeed(video, Math.max(0.1, video.playbackRate - 0.1));
                break;
            case 'z':
                if (parseFloat(video.playbackRate.toFixed(1)) !== 1.0) {
                    // Currently fast -> Reset to 1.0x normal speed
                    updateSpeed(video, 1.0);
                } else {
                    // Currently normal -> Apply the last modified speed
                    updateSpeed(video, lastModifiedSpeed);
                }
                break;

            case '1': case '2': case '3': case '4': case '5': case '6':
                updateSpeed(video, parseInt(e.key));
                break;

            case 'd':
                video.currentTime -= (1 / currentFPS);
                showToast(video, `Prev Frame (${currentFPS}fps)`);
                break;
            case 'f':
                video.currentTime += (1 / currentFPS);
                showToast(video, `Next Frame (${currentFPS}fps)`);
                break;

            case 'arrowright':
                video.currentTime += e.shiftKey ? 90 : 5;
                showToast(video, `Forward ${e.shiftKey ? 90 : 5}s`);
                break;
            case 'arrowleft':
                video.currentTime -= e.shiftKey ? 90 : 5;
                showToast(video, `Backward ${e.shiftKey ? 90 : 5}s`);
                break;
            case 'arrowup':
                updateVolume(video, Math.min(1, video.volume + 0.1));
                break;
            case 'arrowdown':
                updateVolume(video, Math.max(0, video.volume - 0.1));
                break;

            default:
                handled = false;
        }

        if (handled) {
            e.preventDefault();
            e.stopPropagation();
        }
    }, true);

    // Apply saved settings on video load
    window.addEventListener('loadedmetadata', function(e) {
        if (e.target.tagName === 'VIDEO') {
            const video = e.target;

            // 1. Apply Global Speed
            if (enableGlobalSpeed) {
                video.playbackRate = globalPlaybackSpeed;
            }

            // 2. Apply Global Volume
            if (enableGlobalVolume) {
                video.volume = globalVolume;
            }

            // 3. Apply Saved Progress
            safeRestoreProgress(video);

            // 4. Auto-Play
            if (enableAutoPlay) {
                const playPromise = video.play();
                if (playPromise !== undefined) {
                    playPromise.catch(() => {
                        // Suppress console spam; browser policies block autoplay often.
                    });
                }
            }
        }
    }, true);

    // Explicit hook interception directly onto play events captures lazy-loaded content switches on SPAs
    window.addEventListener('play', function(e) {
        if (e.target.tagName === 'VIDEO') {
            safeRestoreProgress(e.target);
        }
    }, true);

    // Continuous progress saving - Throttled when page is hidden
    if (enableResumeProgress) {
        setInterval(() => {
            if (document.hidden) return;

            const video = getVideo();
            if (video && !video.paused && video.currentTime > 5) {
                const progressKey = getUniqueVideoKey(video);
                if (video.duration - video.currentTime > 5) {
                    localStorage.setItem(progressKey, video.currentTime);
                } else {
                    localStorage.removeItem(progressKey);
                }
            }
        }, 2000);
    }
})();