Greasy Fork

Greasy Fork is available in English.

视频倍速播放增强版

长按右方向键倍速播放,松开恢复原速。按+/-键调整倍速,按]/[键快速调整倍速,按P键恢复1.0倍速。上/下方向键调节音量,回车键切换全屏。左/右方向键快退/快进5秒。支持YouTube、Bilibili等大多数视频网站,可通过点击选择控制多个视频。

当前为 2025-06-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         视频倍速播放增强版
// @name:en      Enhanced Video Speed Controller
// @namespace    http://tampermonkey.net/
// @version      1.3.7
// @description  长按右方向键倍速播放,松开恢复原速。按+/-键调整倍速,按]/[键快速调整倍速,按P键恢复1.0倍速。上/下方向键调节音量,回车键切换全屏。左/右方向键快退/快进5秒。支持YouTube、Bilibili等大多数视频网站,可通过点击选择控制多个视频。
// @description:en  Hold right arrow key for speed playback, release to restore. Press +/- to adjust speed, press ]/[ for quick speed adjustment, press P to restore 1.0x speed. Up/Down arrows control volume, Enter toggles fullscreen. Left/Right arrows for 5s rewind/forward. Supports YouTube, Bilibili and most video websites. Click to select which video to control when multiple videos exist.
// @author       ternece
// @license      MIT
// @match        *://*.youtube.com/*
// @match        *://*.bilibili.com/video/*
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// ==/UserScript==

(function () {
    "use strict";

    // 默认设置
    const DEFAULT_SETTINGS = {
        defaultRate: 1.0,    // 默认播放速度
        targetRate: 2.5,     // 长按右键时的倍速
        quickRateStep: 0.5,  // 按[]键调整速度的步长
        targetRateStep: 0.5  // 按 +/- 键调整目标倍速的步长
    };

    // 显示通知 (保留在外部,因为它依赖 GM_notification)
    function showNotification(message) {
        if (typeof GM_notification !== 'undefined') {
            GM_notification({
                text: message,
                title: '视频倍速控制器',
                timeout: 3000
            });
        } else {
            // 如果 GM_notification 不可用,则使用浮动消息作为备用
            showFloatingMessage(message);
        }
    }

    // 显示浮动提示 (保留在外部,因为它是一个独立的UI工具函数)
    function showFloatingMessage(message) {
        const messageElement = document.createElement("div");
        messageElement.textContent = message;
        messageElement.style.position = "fixed";
        messageElement.style.top = "10px";
        messageElement.style.left = "50%";
        messageElement.style.transform = "translateX(-50%)";
        messageElement.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
        messageElement.style.color = "white";
        messageElement.style.padding = "8px 16px";
        messageElement.style.borderRadius = "4px";
        messageElement.style.zIndex = "10000";
        messageElement.style.fontFamily = "Arial, sans-serif";
        messageElement.style.fontSize = "14px";
        messageElement.style.transition = "opacity 0.5s ease-out";
        document.body.appendChild(messageElement);
        setTimeout(() => {
            messageElement.style.opacity = "0";
            setTimeout(() => {
                document.body.removeChild(messageElement);
            }, 500);
        }, 2000);
    }

    class VideoController {
        constructor() {
            // 调试开关
            this.DEBUG = false;
            // 长按判定时间(毫秒)
            this.LONG_PRESS_DELAY = 200;

            // 1. 状态 (State)
            this.settings = {
                defaultRate: GM_getValue('defaultRate', DEFAULT_SETTINGS.defaultRate),
                targetRate: GM_getValue('targetRate', DEFAULT_SETTINGS.targetRate),
                quickRateStep: GM_getValue('quickRateStep', DEFAULT_SETTINGS.quickRateStep),
                targetRateStep: GM_getValue('targetRateStep', DEFAULT_SETTINGS.targetRateStep)
            };
            this.tempEnabledDomains = GM_getValue('tempEnabledDomains', []);
            this.currentDomain = window.location.hostname;
            this.currentUrl = location.href;
            this.lastManualRateChangeTime = 0;
            this.activeVideo = null;
            this.videoControlButtons = new Map();
            this.rightKeyTimer = null;
            this.downCount = 0;
            this.originalRate = 1.0;
            this.targetRate = this.settings.targetRate;
            this.currentQuickRate = 1.0;
            this.keyHandlers = {};

            // 监听器和观察器引用
            this.keydownListener = null;
            this.keyupListener = null;
            this.videoObserver = null;
            this.urlObserver = null;
            this.videoChangeObserver = null;
            this.activeObservers = new Set();
            this._initializeKeyHandlers();
        }

        // 2. 核心启动与检查逻辑
        start() {
            if (!this.shouldEnableScript()) {
                this.registerEnableCommand();
                return;
            }

            this.registerMenuCommands();
            this.startInitializationProcess();
        }

        shouldEnableScript() {
            if (this.currentDomain.includes('youtube.com') ||
                (this.currentDomain.includes('bilibili.com') && window.location.pathname.includes('/video/'))) {
                return true;
            }
            return this.tempEnabledDomains.includes(this.currentDomain);
        }

        // 3. 菜单命令注册
        registerEnableCommand() {
            GM_registerMenuCommand('在当前网站启用视频倍速控制', () => {
                if (!this.tempEnabledDomains.includes(this.currentDomain)) {
                    this.tempEnabledDomains.push(this.currentDomain);
                    GM_setValue('tempEnabledDomains', this.tempEnabledDomains);
                    showNotification(`已在 ${this.currentDomain} 启用视频倍速控制,请刷新页面`);
                } else {
                    showNotification(`${this.currentDomain} 已经在启用列表中`);
                }
            });
        }

        registerMenuCommands() {
            GM_registerMenuCommand('设置默认播放速度', () => this.updateSetting('defaultRate', '请输入默认播放速度 (0.1-16)'));
            GM_registerMenuCommand('设置长按右键倍速', () => this.updateSetting('targetRate', '请输入长按右键时的倍速 (0.1-16)'));
            GM_registerMenuCommand('设置快速调速步长', () => this.updateSetting('quickRateStep', '请输入按 [ 或 ] 键调整速度的步长 (0.1-3)', 3));
            GM_registerMenuCommand('设置目标倍速调整步长', () => this.updateSetting('targetRateStep', '请输入按 +/- 键调整目标倍速的步长 (0.1-16)'));

            if (this.tempEnabledDomains.includes(this.currentDomain)) {
                GM_registerMenuCommand('从临时启用列表中移除当前网站', () => {
                    const index = this.tempEnabledDomains.indexOf(this.currentDomain);
                    if (index !== -1) {
                        this.tempEnabledDomains.splice(index, 1);
                        GM_setValue('tempEnabledDomains', this.tempEnabledDomains);
                        showNotification(`已从临时启用列表中移除 ${this.currentDomain},请刷新页面`);
                    }
                });
            }

            GM_registerMenuCommand('查看所有临时启用的网站', () => {
                if (this.tempEnabledDomains.length === 0) {
                    alert('当前没有临时启用的网站');
                } else {
                    alert('临时启用的网站列表:\n\n' + this.tempEnabledDomains.join('\n'));
                }
            });
        }
        
        updateSetting(key, promptMessage, max = 16) {
            const newValue = prompt(promptMessage, this.settings[key]);
            if (newValue !== null) {
                const value = parseFloat(newValue);
                if (!isNaN(value) && value >= 0.1 && value <= max) {
                    this.settings[key] = value;
                    GM_setValue(key, value);
                    showFloatingMessage(`设置已更新: ${value}`);
                    if (key === 'defaultRate' && this.activeVideo) {
                        this.activeVideo.playbackRate = value;
                    }
                } else {
                    alert(`请输入有效的值 (0.1-${max})`);
                }
            }
        }


        // 4. 初始化流程
        async startInitializationProcess() {
            let retryCount = 0;
            const maxRetries = 3;

            const tryInit = async () => {
                try {
                    await this.init();
                    this.watchUrlChange();
                } catch (error) {
                    if (error && (error.type === "no_video" || error.type === "timeout")) {
                        return; // 停止重试
                    }
                    console.warn("启动失败:", error);
                    if (retryCount < maxRetries) {
                        retryCount++;
                        setTimeout(tryInit, 2000);
                    }
                }
            };
            tryInit();
        }

        async init() {
            this.cleanup();

            try {
                let video = await this.waitForVideoElement();
                if (!video) {
                    const deepVideos = this.deepFindVideoElements();
                    if (deepVideos.length > 0) {
                        video = deepVideos[0];
                        this.setupVideos(deepVideos);
                        showFloatingMessage(`通过深度查找发现了 ${deepVideos.length} 个视频`);
                    } else {
                        throw { type: "no_video" };
                    }
                }

                console.log("找到视频元素:", video);
                this.activeVideo = video;
                this.setupObservers();
                this.setupEventListeners();

            } catch (error) {
                console.error("初始化失败:", error);
                if (error && (error.type === "timeout" || error.type === "no_video")) {
                    setTimeout(() => this.init().catch(console.error), 5000);
                }
                throw error;
            }
        }

        // 5. 清理与监听
        cleanup() {
            if (this.keydownListener) {
                document.removeEventListener("keydown", this.keydownListener, true);
                this.keydownListener = null;
            }
            if (this.keyupListener) {
                document.removeEventListener("keyup", this.keyupListener, true);
                this.keyupListener = null;
            }
            this.activeObservers.forEach(observer => observer.disconnect());
            this.activeObservers.clear();
            this.videoControlButtons.forEach(button => button.remove());
            this.videoControlButtons.clear();
            this.activeVideo = null;
        }

        setupObservers() {
            // 观察新视频
            this.videoObserver = new MutationObserver(() => this.detectAndSetupVideos());
            this.videoObserver.observe(document.body, { childList: true, subtree: true });
            this.activeObservers.add(this.videoObserver);

            // 观察视频元素变化
            if (this.activeVideo && this.activeVideo.parentElement) {
                this.videoChangeObserver = new MutationObserver((mutations) => {
                    const hasVideoChanges = mutations.some(m => Array.from(m.removedNodes).some(n => n.tagName === 'VIDEO'));
                    if (hasVideoChanges) {
                        console.log("视频元素变化,重新初始化");
                        this.init().catch(console.error);
                    }
                });
                this.videoChangeObserver.observe(this.activeVideo.parentElement, { childList: true, subtree: true });
                this.activeObservers.add(this.videoChangeObserver);
            }
        }
        
        watchUrlChange() {
            const handleStateChange = () => {
                if (location.href !== this.currentUrl) {
                    this.currentUrl = location.href;
                    console.log("URL变化,重新初始化");
                    setTimeout(() => this.init().catch(console.error), 1000);
                }
            };

            // 使用 History API 监听
            const originalPushState = history.pushState;
            history.pushState = function() {
                originalPushState.apply(this, arguments);
                handleStateChange();
            };

            const originalReplaceState = history.replaceState;
            history.replaceState = function() {
                originalReplaceState.apply(this, arguments);
                handleStateChange();
            };
            
            window.addEventListener('popstate', handleStateChange);
            
            // 使用 MutationObserver 作为备用
            this.urlObserver = new MutationObserver(handleStateChange);
            this.urlObserver.observe(document.body, { childList: true, subtree: true });
            this.activeObservers.add(this.urlObserver);
        }


        // 6. 事件监听器设置
        setupEventListeners() {
            this.keydownListener = this.handleKeyDown.bind(this);
            this.keyupListener = this.handleKeyUp.bind(this);
            document.addEventListener("keydown", this.keydownListener, true);
            document.addEventListener("keyup", this.keyupListener, true);
        }

        // 7. 视频查找与设置
        waitForVideoElement() {
            return new Promise((resolve, reject) => {
                const maxAttempts = 20;
                let attempts = 0;
                const check = () => {
                    const video = this.detectAndSetupVideos();
                    if (video) {
                        observer.disconnect();
                        resolve(video);
                    } else if (++attempts >= maxAttempts) {
                        observer.disconnect();
                        reject({ type: "no_video" });
                    }
                };
                const observer = new MutationObserver(check);
                observer.observe(document.body, { childList: true, subtree: true });
                this.activeObservers.add(observer);
                check(); // 立即检查
                setTimeout(() => {
                    observer.disconnect();
                    reject({ type: "timeout" });
                }, 10000);
            });
        }
        
        deepFindVideoElements() {
            console.log('开始深度查找视频元素...');
            const foundVideos = new Set();
            const find = (element, depth = 0) => {
                if (depth > 10) return;
                if (element.tagName === 'VIDEO') foundVideos.add(element);
                if (element.shadowRoot) find(element.shadowRoot, depth + 1);
                if (element.contentDocument) find(element.contentDocument, depth + 1);
                Array.from(element.children || []).forEach(child => find(child, depth + 1));
            };
            find(document.body);
            console.log(`深度查找完成,共找到 ${foundVideos.size} 个视频元素`);
            return Array.from(foundVideos);
        }
        
        detectAndSetupVideos() {
            const videos = this.findAllVideos();
            if (videos.length === 0) return null;
            this.setupVideos(videos);
            return this.activeVideo || videos[0];
        }

        findAllVideos() {
            const allVideos = new Set(document.querySelectorAll('video'));
            const findIn = (root) => {
                try {
                    root.querySelectorAll('video').forEach(v => allVideos.add(v));
                    root.querySelectorAll('iframe').forEach(f => {
                         try {
                            if (f.contentDocument) findIn(f.contentDocument);
                         } catch(e) {/* cross-origin */}
                    });
                    root.querySelectorAll('*').forEach(el => {
                        if (el.shadowRoot) findIn(el.shadowRoot);
                    });
                } catch(e) {/* ignore */}
            };
            findIn(document);
            return Array.from(allVideos);
        }

        setupVideos(videos) {
            if (videos.length === 1) {
                const video = videos[0];
                if (video.readyState >= 1 && !this.activeVideo) {
                    this.activeVideo = video;
                    this.setDefaultRate(video);
                }
            } else if (videos.length > 1) {
                // 对于B站和油管,进行特殊的主视频判断
                const isBilibili = this.currentDomain.includes('bilibili.com');
                const isYoutube = this.currentDomain.includes('youtube.com');

                if (isBilibili || isYoutube) {
                     if (!this.activeVideo || !videos.includes(this.activeVideo)) {
                        let mainVideo;
                        if(isBilibili) mainVideo = videos.find(v => !v.paused) || videos.find(v => v.getBoundingClientRect().width > 400);
                        if(isYoutube) mainVideo = videos.find(v => v.classList.contains('html5-main-video'));
                        this.activeVideo = mainVideo || videos[0];
                        this.setDefaultRate(this.activeVideo);
                    }
                } else {
                    // 其他网站,创建控制按钮
                    videos.forEach((video, index) => {
                        if (!this.videoControlButtons.has(video) && video.readyState >= 1) {
                            this.createVideoControlButton(video, index + 1);
                            this.setDefaultRate(video);
                            if (!this.activeVideo) this.activeVideo = video;
                        }
                    });
                }
            }
        }
        
        setDefaultRate(video) {
            if (Date.now() - this.lastManualRateChangeTime > 5000) {
                video.playbackRate = this.settings.defaultRate;
            }
        }

        createVideoControlButton(video, index) {
            const button = document.createElement('div');
            Object.assign(button.style, {
                position: 'absolute', top: '10px', left: '10px',
                backgroundColor: 'rgba(0, 0, 0, 0.6)', color: 'white',
                padding: '5px 10px', borderRadius: '4px', fontSize: '12px',
                fontFamily: 'Arial, sans-serif', cursor: 'pointer', zIndex: '9999',
                transition: 'background-color 0.3s', userSelect: 'none'
            });
            button.innerHTML = `<span>视频 ${index}</span>`;

            if (!this.activeVideo) {
                this.activeVideo = video;
                button.style.backgroundColor = 'rgba(0, 128, 255, 0.8)';
            }

            button.addEventListener('click', () => {
                this.videoControlButtons.forEach(btn => btn.style.backgroundColor = 'rgba(0, 0, 0, 0.6)');
                this.activeVideo = video;
                button.style.backgroundColor = 'rgba(0, 128, 255, 0.8)';
                showFloatingMessage(`已切换到视频 ${index} 控制`);
            });

            const container = video.parentElement || document.body;
            const style = window.getComputedStyle(container);
            if (style.position === 'static') container.style.position = 'relative';
            container.appendChild(button);
            this.videoControlButtons.set(video, button);
        }

        // 8. 按键事件处理
        handleKeyDown(e) {
            const path = e.composedPath();
            const isInputFocused = path.some(el => el.isContentEditable || ['INPUT', 'TEXTAREA'].includes(el.tagName));
            if (isInputFocused || !this.activeVideo) {
                return;
            }
        
            const handler = this.keyHandlers[e.code];
            if (handler) {
                e.preventDefault();
                e.stopImmediatePropagation();
                handler();
            }
        }

        handleKeyUp(e) {
            if (e.code === 'ArrowRight') {
                clearTimeout(this.rightKeyTimer);
                this.rightKeyTimer = null;
                
                if (this.downCount < 3) { //判定为短按
                    this.seek(5);
                } else { //判定为长按
                     if(this.activeVideo) {
                        this.activeVideo.playbackRate = this.originalRate;
                        showFloatingMessage(`恢复播放速度: ${this.originalRate.toFixed(1)}x`);
                     }
                }
                this.downCount = 0;
            }
        }
        
        // 9. 按键处理器和具体功能实现
        _initializeKeyHandlers() {
            this.keyHandlers = {
                'ArrowUp': this._handleVolumeUp.bind(this),
                'ArrowDown': this._handleVolumeDown.bind(this),
                'Enter': this._handleToggleFullScreen.bind(this),
                'Space': this._handleTogglePlayPause.bind(this),
                'ArrowLeft': this._handleSeekBackward.bind(this),
                'ArrowRight': this._handleRightArrowPress.bind(this),
                'Equal': this._handleIncreaseTargetRate.bind(this),
                'Minus': this._handleDecreaseTargetRate.bind(this),
                'BracketRight': this._handleIncreasePlaybackRate.bind(this),
                'BracketLeft': this._handleDecreasePlaybackRate.bind(this),
                'KeyP': this._handleResetPlaybackRate.bind(this),
                'Comma': this._handleFrameStepBackward.bind(this),
                'Period': this._handleFrameStepForward.bind(this),
            };
        }

        _handleVolumeUp() { this.adjustVolume(0.1); }
        _handleVolumeDown() { this.adjustVolume(-0.1); }
        _handleToggleFullScreen() { this.toggleFullScreen(); }
        _handleTogglePlayPause() { this.togglePlayPause(); }
        _handleSeekBackward() { this.seek(-5); }
        _handleRightArrowPress() { this.handleRightArrowPress(); }
        _handleIncreaseTargetRate() { this.adjustTargetRate(this.settings.targetRateStep); }
        _handleDecreaseTargetRate() { this.adjustTargetRate(-this.settings.targetRateStep); }
        _handleIncreasePlaybackRate() { this.adjustPlaybackRate(this.settings.quickRateStep); }
        _handleDecreasePlaybackRate() { this.adjustPlaybackRate(-this.settings.quickRateStep); }
        _handleResetPlaybackRate() { this.resetPlaybackRate(); }
        _handleFrameStepBackward() { this.frameStep(-1); }
        _handleFrameStepForward() { this.frameStep(1); }

        adjustVolume(delta) {
            this.activeVideo.volume = Math.max(0, Math.min(1, this.activeVideo.volume + delta));
            showFloatingMessage(`音量:${Math.round(this.activeVideo.volume * 100)}%`);
        }

        toggleFullScreen() {
            let fsButton = null;
            // 针对B站的特殊处理,优先寻找真正的全屏按钮
            if (this.currentDomain.includes('bilibili.com')) {
                // '.bpx-player-ctrl-full' 是新版播放器的浏览器全屏按钮
                // '.bilibili-player-video-btn-fullscreen' 是旧版的
                fsButton = document.querySelector('.bpx-player-ctrl-full') ||
                           document.querySelector('.bilibili-player-video-btn-fullscreen');
            }
            // 针对YouTube
            else if (this.currentDomain.includes('youtube.com')) {
                fsButton = document.querySelector('.ytp-fullscreen-button');
            }

            // 如果特定网站的按钮被找到,则点击
            if (fsButton) {
                fsButton.click();
                return;
            }

            // 通用备用方案:使用原生API
            console.log('未找到特定网站的全屏按钮,使用原生API。');
            if (!document.fullscreenElement) {
                if (this.activeVideo.requestFullscreen) {
                    this.activeVideo.requestFullscreen();
                } else if (this.activeVideo.webkitRequestFullscreen) {
                    this.activeVideo.webkitRequestFullscreen();
                } else if (this.activeVideo.msRequestFullscreen) {
                    this.activeVideo.msRequestFullscreen();
                }
                showFloatingMessage('进入全屏');
            } else {
                if (document.exitFullscreen) {
                    document.exitFullscreen();
                } else if (document.webkitExitFullscreen) {
                    document.webkitExitFullscreen();
                } else if (document.msExitFullscreen) {
                    document.msExitFullscreen();
                }
                showFloatingMessage('退出全屏');
            }
        }

        togglePlayPause() {
            if (this.activeVideo.paused) {
                this.activeVideo.play();
                showFloatingMessage('播放');
            } else {
                this.activeVideo.pause();
                showFloatingMessage('暂停');
            }
        }
        
        seek(delta) {
            if (this.activeVideo.paused) this.activeVideo.play();
            this.activeVideo.currentTime = Math.max(0, this.activeVideo.currentTime + delta);
            showFloatingMessage(`快${delta > 0 ? '进' : '退'} ${Math.abs(delta)} 秒`);
        }
        
        // 此方法逻辑复杂,保留原名,仅在 handler 中调用
        handleRightArrowPress() {
            if (this.activeVideo.paused) this.activeVideo.play();

            if (this.downCount === 0) {
                this.originalRate = this.activeVideo.playbackRate;
                this.rightKeyTimer = setTimeout(() => {
                    this.activeVideo.playbackRate = this.targetRate;
                    showFloatingMessage(`倍速播放: ${this.targetRate}x`);
                    this.downCount = 3; // 设置为长按状态
                }, this.LONG_PRESS_DELAY);
            }
            this.downCount++;
        }

        adjustTargetRate(delta) {
            this.targetRate = Math.max(0.1, Math.min(16, this.targetRate + delta));
            this.lastManualRateChangeTime = Date.now();
            showFloatingMessage(`目标倍速设置为: ${this.targetRate.toFixed(1)}x`);
        }
        
        adjustPlaybackRate(delta) {
            const newRate = Math.max(0.1, Math.min(16, this.activeVideo.playbackRate + delta));
            this.activeVideo.playbackRate = newRate;
            this.lastManualRateChangeTime = Date.now();
            showFloatingMessage(`播放速度: ${newRate.toFixed(1)}x`);
        }
        
        resetPlaybackRate() {
            this.activeVideo.playbackRate = 1.0;
            this.lastManualRateChangeTime = Date.now();
            showFloatingMessage(`播放速度重置为 1.0x`);
        }
        
        frameStep(direction) {
            if (this.activeVideo.paused) {
                 this.activeVideo.currentTime += (direction / 30); // 假设30fps
                 showFloatingMessage(direction > 0 ? `下一帧` : `上一帧`);
            }
        }
    }

    // 启动脚本
    const controller = new VideoController();
    controller.start();

})();