Greasy Fork

Greasy Fork is available in English.

视频倍速播放增强版

长按右方向键倍速播放,松开恢复原速。按+/-键调整倍速,按]/[键快速调整倍速,按P键恢复默认速度。上/下方向键调节音量,回车键切换全屏。左/右方向键快退/快进5秒。支持YouTube、Bilibili等大多数视频网站。脚本会自动检测页面中的iframe视频并启用相应控制。

安装此脚本
作者推荐脚本

您可能也喜欢视频倍速播放

安装此脚本
// ==UserScript==
// @name         视频倍速播放增强版
// @name:en      Enhanced Video Speed Controller
// @namespace    http://tampermonkey.net/
// @version      1.5.4
// @description  长按右方向键倍速播放,松开恢复原速。按+/-键调整倍速,按]/[键快速调整倍速,按P键恢复默认速度。上/下方向键调节音量,回车键切换全屏。左/右方向键快退/快进5秒。支持YouTube、Bilibili等大多数视频网站。脚本会自动检测页面中的iframe视频并启用相应控制。
// @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 default speed. Up/Down arrows control volume, Enter toggles fullscreen. Left/Right arrows for 5s rewind/forward. Supports most sites. The script automatically detects iframe videos on the page and enables control.
// @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  // 按 +/- 键调整目标倍速的步长
    };

    // 通用配置
    const CONFIG = {
        SEEK_STEP_SECONDS: 5,           // 快进/快退的秒数
        VOLUME_STEP: 0.1,               // 音量调整步长
        DEFAULT_FPS: 30,                // 默认视频帧率 (用于逐帧操作)
        SHORT_PRESS_MAX_COUNT: 3,       // 短按判断的按键计数阈值

        // 超时与延迟
        INIT_RETRY_DELAY: 5000,         // 初始化重试延迟
        URL_CHANGE_INIT_DELAY: 1000,    // URL 变化后初始化延迟
        WAIT_FOR_VIDEO_TIMEOUT: 10000,  // 等待视频元素超时时间

        // 数值限制
        MAX_RATE: 16,                   // 最大允许的播放速度
        MAX_QUICK_RATE_STEP: 3          // “快速调速步长”的最大值
    };

    // 特定网站的配置
    const SITE_SPECIFIC_CONFIG = {
        'youtube.com': {
            mainVideoSelector: '.html5-main-video',
            fullscreenButtonSelector: '.ytp-fullscreen-button'
        },
        'bilibili.com': {
            // 宽度大于400px通常是主播放器
            mainVideoPredicate: video => video.getBoundingClientRect().width > 400,
            // 新版 '.bpx-player-ctrl-full', 旧版 '.bilibili-player-video-btn-fullscreen'
            fullscreenButtonSelector: '.bpx-player-ctrl-full, .bilibili-player-video-btn-fullscreen'
        }
    };

    // 显示通知 (保留在外部,因为它依赖 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);
    }

    // 显示域名管理弹窗(分层级)
    function showDomainManager(groups, controller) {
        // 如果在iframe中运行,不显示弹窗(避免与主页面重复)
        if (window.self !== window.top) {
            showFloatingMessage('此功能仅在主页面可用');
            return;
        }

        // 创建遮罩层
        const overlay = document.createElement("div");
        overlay.style.position = "fixed";
        overlay.style.top = "0";
        overlay.style.left = "0";
        overlay.style.width = "100%";
        overlay.style.height = "100%";
        overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
        overlay.style.zIndex = "10001";
        overlay.style.display = "flex";
        overlay.style.justifyContent = "center";
        overlay.style.alignItems = "center";

        // 创建弹窗容器
        const modal = document.createElement("div");
        modal.style.backgroundColor = "white";
        modal.style.borderRadius = "8px";
        modal.style.padding = "0";
        modal.style.maxWidth = "700px";
        modal.style.width = "90%";
        modal.style.maxHeight = "80vh";
        modal.style.boxShadow = "0 4px 20px rgba(0, 0, 0, 0.3)";
        modal.style.display = "flex";
        modal.style.flexDirection = "column";

        // 创建弹窗头部
        const header = document.createElement("div");
        header.style.padding = "20px";
        header.style.borderBottom = "1px solid #eee";
        header.style.display = "flex";
        header.style.justifyContent = "space-between";
        header.style.alignItems = "center";
        header.style.backgroundColor = "#f8f9fa";
        header.style.borderTopLeftRadius = "8px";
        header.style.borderTopRightRadius = "8px";

        const title = document.createElement("h2");
        title.textContent = `已启用的网站 (${groups.length})`;
        title.style.margin = "0";
        title.style.fontSize = "18px";
        title.style.fontWeight = "600";
        title.style.color = "#333";

        const buttonContainer = document.createElement("div");
        buttonContainer.style.display = "flex";
        buttonContainer.style.gap = "10px";

        // 一键清空按钮
        const clearAllBtn = document.createElement("button");
        clearAllBtn.textContent = "清空所有";
        clearAllBtn.style.padding = "8px 16px";
        clearAllBtn.style.backgroundColor = "#dc3545";
        clearAllBtn.style.color = "white";
        clearAllBtn.style.border = "none";
        clearAllBtn.style.borderRadius = "4px";
        clearAllBtn.style.cursor = "pointer";
        clearAllBtn.style.fontSize = "14px";
        clearAllBtn.style.fontWeight = "500";
        clearAllBtn.onmouseover = () => {
            clearAllBtn.style.backgroundColor = "#c82333";
        };
        clearAllBtn.onmouseout = () => {
            clearAllBtn.style.backgroundColor = "#dc3545";
        };
        clearAllBtn.onclick = () => {
            if (confirm("确定要清空所有临时启用的网站吗?\n\n注意:YouTube 和 Bilibili 不会受影响")) {
                controller.tempEnabledDomainGroups = [];
                controller.tempEnabledDomains = [];
                GM_setValue('tempEnabledDomainGroups', controller.tempEnabledDomainGroups);
                GM_setValue('tempEnabledDomains', controller.tempEnabledDomains);
                document.body.removeChild(overlay);
                showNotification("✅ 已清空临时启用列表\n请刷新页面");
            }
        };

        // 关闭按钮
        const closeBtn = document.createElement("button");
        closeBtn.textContent = "×";
        closeBtn.style.padding = "8px 12px";
        closeBtn.style.backgroundColor = "transparent";
        closeBtn.style.color = "#666";
        closeBtn.style.border = "none";
        closeBtn.style.borderRadius = "4px";
        closeBtn.style.cursor = "pointer";
        closeBtn.style.fontSize = "24px";
        closeBtn.style.fontWeight = "300";
        closeBtn.style.lineHeight = "1";
        closeBtn.onmouseover = () => {
            closeBtn.style.backgroundColor = "#e9ecef";
        };
        closeBtn.onmouseout = () => {
            closeBtn.style.backgroundColor = "transparent";
        };
        closeBtn.onclick = () => {
            document.body.removeChild(overlay);
        };

        buttonContainer.appendChild(clearAllBtn);
        buttonContainer.appendChild(closeBtn);
        header.appendChild(title);
        header.appendChild(buttonContainer);

        // 创建内容区域
        const content = document.createElement("div");
        content.style.padding = "20px";
        content.style.overflowY = "auto";
        content.style.flex = "1";

        if (groups.length === 0) {
            const emptyMsg = document.createElement("div");
            emptyMsg.textContent = "当前没有临时启用的网站";
            emptyMsg.style.textAlign = "center";
            emptyMsg.style.color = "#999";
            emptyMsg.style.padding = "40px 0";
            emptyMsg.style.fontSize = "16px";
            content.appendChild(emptyMsg);
        } else {
            const groupsList = document.createElement("div");
            groupsList.style.display = "flex";
            groupsList.style.flexDirection = "column";
            groupsList.style.gap = "15px";

            groups.forEach((group, groupIndex) => {
                // 创建主分组容器
                const groupContainer = document.createElement("div");
                groupContainer.style.border = "2px solid #dee2e6";
                groupContainer.style.borderRadius = "8px";
                groupContainer.style.overflow = "hidden";

                // 主域名行
                const mainDomainRow = document.createElement("div");
                mainDomainRow.style.display = "flex";
                mainDomainRow.style.justifyContent = "space-between";
                mainDomainRow.style.alignItems = "center";
                mainDomainRow.style.padding = "15px";
                mainDomainRow.style.backgroundColor = "#e7f3ff";
                mainDomainRow.style.borderBottom = group.iframes.length > 0 ? "1px solid #dee2e6" : "none";

                // 展开/折叠按钮
                const expandBtn = document.createElement("button");
                expandBtn.textContent = group.iframes.length > 0 ? (group.expanded ? '▼' : '▶') : '•';
                expandBtn.style.padding = "4px 8px";
                expandBtn.style.backgroundColor = "transparent";
                expandBtn.style.color = "#0066cc";
                expandBtn.style.border = "none";
                expandBtn.style.borderRadius = "4px";
                expandBtn.style.cursor = group.iframes.length > 0 ? "pointer" : "default";
                expandBtn.style.fontSize = "14px";
                expandBtn.style.fontWeight = "bold";
                expandBtn.disabled = group.iframes.length === 0;
                expandBtn.onclick = () => {
                    group.expanded = !group.expanded;
                    document.body.removeChild(overlay);
                    showDomainManager(groups, controller);
                };

                // 主域名
                const mainDomainSpan = document.createElement("span");
                mainDomainSpan.textContent = `${groupIndex + 1}. ${group.mainDomain}`;
                mainDomainSpan.style.fontFamily = "Monaco, Consolas, monospace";
                mainDomainSpan.style.fontSize = "15px";
                mainDomainSpan.style.fontWeight = "600";
                mainDomainSpan.style.color = "#0066cc";
                mainDomainSpan.style.flex = "1";
                mainDomainSpan.style.marginLeft = "10px";

                // 删除分组按钮
                const deleteGroupBtn = document.createElement("button");
                deleteGroupBtn.textContent = "删除整个分组";
                deleteGroupBtn.style.padding = "6px 12px";
                deleteGroupBtn.style.backgroundColor = "#dc3545";
                deleteGroupBtn.style.color = "white";
                deleteGroupBtn.style.border = "none";
                deleteGroupBtn.style.borderRadius = "4px";
                deleteGroupBtn.style.cursor = "pointer";
                deleteGroupBtn.style.fontSize = "13px";
                deleteGroupBtn.onmouseover = () => {
                    deleteGroupBtn.style.backgroundColor = "#c82333";
                };
                deleteGroupBtn.onmouseout = () => {
                    deleteGroupBtn.style.backgroundColor = "#dc3545";
                };
                deleteGroupBtn.onclick = () => {
                    if (confirm(`确定要删除分组 "${group.mainDomain}" 及其所有iframe域名吗?`)) {
                        controller.deleteDomainGroup(group.mainDomain);
                        document.body.removeChild(overlay);
                        showNotification(`已删除分组:${group.mainDomain},请刷新页面`);
                    }
                };

                mainDomainRow.appendChild(expandBtn);
                mainDomainRow.appendChild(mainDomainSpan);
                mainDomainRow.appendChild(deleteGroupBtn);

                groupContainer.appendChild(mainDomainRow);

                // iframe域名列表
                if (group.expanded && group.iframes.length > 0) {
                    const iframesContainer = document.createElement("div");
                    iframesContainer.style.backgroundColor = "#f8f9fa";
                    iframesContainer.style.padding = "10px 20px";

                    const iframesList = document.createElement("div");
                    iframesList.style.display = "flex";
                    iframesList.style.flexDirection = "column";
                    iframesList.style.gap = "8px";

                    group.iframes.forEach((iframeDomain, iframeIndex) => {
                        const iframeRow = document.createElement("div");
                        iframeRow.style.display = "flex";
                        iframeRow.style.justifyContent = "space-between";
                        iframeRow.style.alignItems = "center";
                        iframeRow.style.padding = "8px 12px";
                        iframeRow.style.backgroundColor = "white";
                        iframeRow.style.borderRadius = "4px";
                        iframeRow.style.border = "1px solid #dee2e6";

                        const indent = document.createElement("span");
                        indent.textContent = "  └─ ";
                        indent.style.color = "#666";
                        indent.style.fontSize = "14px";

                        const iframeDomainSpan = document.createElement("span");
                        iframeDomainSpan.textContent = iframeDomain;
                        iframeDomainSpan.style.fontFamily = "Monaco, Consolas, monospace";
                        iframeDomainSpan.style.fontSize = "14px";
                        iframeDomainSpan.style.color = "#333";
                        iframeDomainSpan.style.flex = "1";

                        const deleteIframeBtn = document.createElement("button");
                        deleteIframeBtn.textContent = "删除";
                        deleteIframeBtn.style.padding = "4px 10px";
                        deleteIframeBtn.style.backgroundColor = "#ff6b6b";
                        deleteIframeBtn.style.color = "white";
                        deleteIframeBtn.style.border = "none";
                        deleteIframeBtn.style.borderRadius = "4px";
                        deleteIframeBtn.style.cursor = "pointer";
                        deleteIframeBtn.style.fontSize = "12px";
                        deleteIframeBtn.onmouseover = () => {
                            deleteIframeBtn.style.backgroundColor = "#ee5a5a";
                        };
                        deleteIframeBtn.onmouseout = () => {
                            deleteIframeBtn.style.backgroundColor = "#ff6b6b";
                        };
                        deleteIframeBtn.onclick = () => {
                            controller.removeIframeFromGroup(group.mainDomain, iframeDomain);
                            document.body.removeChild(overlay);
                            showNotification(`已从分组中删除:${iframeDomain},请刷新页面`);
                        };

                        iframeRow.appendChild(indent);
                        iframeRow.appendChild(iframeDomainSpan);
                        iframeRow.appendChild(deleteIframeBtn);
                        iframesList.appendChild(iframeRow);
                    });

                    iframesContainer.appendChild(iframesList);
                    groupContainer.appendChild(iframesContainer);
                }

                groupsList.appendChild(groupContainer);
            });

            content.appendChild(groupsList);
        }

        modal.appendChild(header);
        modal.appendChild(content);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // 点击遮罩层关闭
        overlay.addEventListener("click", (e) => {
            if (e.target === overlay) {
                document.body.removeChild(overlay);
            }
        });
    }

    // 通用防抖函数
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func.apply(this, args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

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

            // 从全局加载配置
            this.config = CONFIG;
            
            // 获取当前网站的特定配置
            this.siteConfig = {};
            for (const domain in SITE_SPECIFIC_CONFIG) {
                if (window.location.hostname.includes(domain)) {
                    this.siteConfig = SITE_SPECIFIC_CONFIG[domain];
                    break;
                }
            }

            // 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)
            };
            // 使用分组数据结构:主域名 -> 包含的iframe域名
            this.tempEnabledDomainGroups = GM_getValue('tempEnabledDomainGroups', []);
            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._keyupCallCount = 0; // 调试:KeyUp调用次数
            this._rightKeyUpHandled = false; // 防止重复处理
            this.originalRate = 1.0;
            this.targetRate = this.settings.targetRate;
            this.currentQuickRate = 1.0;
            this.keyHandlers = {};

            // 监听器和观察器引用
            this.keydownListener = null;
            this.keyupListener = null;
            this.mainObserver = null; // 合并后的主观察器
            this.videoChangeObserver = null;
            this.activeObservers = new Set();

            // 创建防抖版的视频检测函数
            this.debouncedDetectAndSetupVideos = debounce(this.detectAndSetupVideos.bind(this), 500);

            this._initializeKeyHandlers();
        }

        /**
         * 检测并返回所有跨域 iframe 的域名
         * @returns {Array<string>} 域名数组
         */
        detectCrossOriginIframeDomains() {
            const crossDomainIframes = new Set();
            const iframes = document.querySelectorAll('iframe');

            iframes.forEach(iframe => {
                try {
                    const src = iframe.src;
                    if (!src) return; // 跳过没有 src 的 iframe

                    const url = new URL(src);
                    const domain = url.hostname;

                    // 如果不是当前域名,添加到列表
                    if (domain !== this.currentDomain) {
                        crossDomainIframes.add(domain);
                    }
                } catch (e) {
                    // 忽略无效的 src(如 javascript: 协议)
                    console.warn('检测到无效的 iframe src:', iframe.src);
                }
            });

            return Array.from(crossDomainIframes);
        }

        /**
         * 批量启用域名到临时列表
         * @param {Array<string>} domains 域名数组
         * @returns {Array<string>} 新添加的域名数组
         */
        enableDomainsInTempList(domains) {
            const newlyEnabled = [];

            domains.forEach(domain => {
                if (!this.tempEnabledDomains.includes(domain)) {
                    this.tempEnabledDomains.push(domain);
                    newlyEnabled.push(domain);
                }
            });

            if (newlyEnabled.length > 0) {
                GM_setValue('tempEnabledDomains', this.tempEnabledDomains);
            }

            return newlyEnabled;
        }

        /**
         * 创建或更新域名分组
         * @param {string} mainDomain 主域名
         * @param {Array<string>} iframeDomains iframe域名数组
         */
        saveDomainGroup(mainDomain, iframeDomains) {
            // 查找是否已存在该主域名的分组
            const existingIndex = this.tempEnabledDomainGroups.findIndex(g => g.mainDomain === mainDomain);

            if (existingIndex >= 0) {
                // 更新现有分组
                const existingGroup = this.tempEnabledDomainGroups[existingIndex];
                // 合并iframe域名(去重)
                const combinedIframes = [...new Set([...existingGroup.iframes, ...iframeDomains])];
                this.tempEnabledDomainGroups[existingIndex] = {
                    mainDomain,
                    iframes: combinedIframes,
                    createdAt: existingGroup.createdAt,
                    updatedAt: Date.now()
                };
            } else {
                // 创建新分组
                this.tempEnabledDomainGroups.push({
                    mainDomain,
                    iframes: iframeDomains,
                    createdAt: Date.now(),
                    updatedAt: Date.now()
                });
            }

            GM_setValue('tempEnabledDomainGroups', this.tempEnabledDomainGroups);
        }

        /**
         * 删除整个域名分组
         * @param {string} mainDomain 主域名
         */
        deleteDomainGroup(mainDomain) {
            this.tempEnabledDomainGroups = this.tempEnabledDomainGroups.filter(g => g.mainDomain !== mainDomain);
            GM_setValue('tempEnabledDomainGroups', this.tempEnabledDomainGroups);
        }

        /**
         * 从分组中删除单个iframe域名
         * @param {string} mainDomain 主域名
         * @param {string} iframeDomain 要删除的iframe域名
         */
        removeIframeFromGroup(mainDomain, iframeDomain) {
            const group = this.tempEnabledDomainGroups.find(g => g.mainDomain === mainDomain);
            if (group) {
                group.iframes = group.iframes.filter(d => d !== iframeDomain);
                group.updatedAt = Date.now();
                GM_setValue('tempEnabledDomainGroups', this.tempEnabledDomainGroups);
            }
        }

        /**
         * 获取所有启用的域名列表(展平)
         * @returns {Array<string>}
         */
        getAllEnabledDomains() {
            const allDomains = new Set();
            this.tempEnabledDomainGroups.forEach(group => {
                allDomains.add(group.mainDomain);
                group.iframes.forEach(d => allDomains.add(d));
            });
            return Array.from(allDomains);
        }

        // 2. 核心启动与检查逻辑
        start() {
            // 核心菜单命令应该总是可用,无论脚本是否已在此网站启用
            this.registerCoreMenuCommands();

            if (!this.shouldEnableScript()) {
                // 如果未启用,则只注册“启用”命令(已在核心中完成),然后返回
                return;
            }
            
            // 如果已启用,则注册其余的动态菜单命令,并开始初始化
            this.registerDynamicMenuCommands();
            this.initialize();
        }

        shouldEnableScript() {
            // 如果在 iframe 中,检查是否有视频
            if (window.self !== window.top) {
                const hasVideo = document.querySelector('video') !== null;
                if (hasVideo) {
                    return true;
                }
                return false;
            }

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

            // 检查是否在已启用的分组中
            const allDomains = this.getAllEnabledDomains();
            return allDomains.includes(this.currentDomain);
        }

        // 3. 菜单命令注册
        registerEnableCommand() {
            GM_registerMenuCommand('在当前网站启用视频倍速控制', () => {
                // 🔍 检测所有跨域 iframe 域名
                const crossOriginDomains = this.detectCrossOriginIframeDomains();

                // 💾 保存分组数据
                this.saveDomainGroup(this.currentDomain, crossOriginDomains);

                // 💬 生成提示信息
                if (crossOriginDomains.length > 0) {
                    showNotification(
                        `✅ 已启用:\n` +
                        `主域名: ${this.currentDomain}\n` +
                        `iframe: ${crossOriginDomains.join(', ')}\n\n` +
                        `请刷新页面以生效。`
                    );

                    // 打印详细信息到控制台
                    console.log('=== 视频倍速控制器 ===');
                    console.log('主域名:', this.currentDomain);
                    console.log('检测到的跨域 iframe 域名:', crossOriginDomains);
                    console.log('已保存的分组:', this.tempEnabledDomainGroups);
                    console.log('========================');

                } else {
                    showNotification(
                        `✅ 已在 ${this.currentDomain} 启用\n` +
                        `请刷新页面`
                    );
                }
            });
        }

        // 核心菜单命令,应无条件注册
        registerCoreMenuCommands() {
             // 仅在脚本未启用时,才显示"启用"命令
            if (!this.shouldEnableScript()) {
                this.registerEnableCommand();
            }


            GM_registerMenuCommand('查看所有临时启用的网站', () => {
                if (this.tempEnabledDomainGroups.length === 0) {
                    showFloatingMessage('当前没有临时启用的网站');
                } else {
                    // 使用弹窗显示分组列表
                    showDomainManager(this.tempEnabledDomainGroups, this);
                }
            });
        }

        // 动态菜单命令,仅在脚本启用后注册
        registerDynamicMenuCommands() {
            GM_registerMenuCommand('设置默认播放速度', () => this.updateSetting('defaultRate', `请输入默认播放速度 (0.1-${this.config.MAX_RATE})`));
            GM_registerMenuCommand('设置长按右键倍速', () => this.updateSetting('targetRate', `请输入长按右键时的倍速 (0.1-${this.config.MAX_RATE})`));
            GM_registerMenuCommand('设置快速调速步长', () => this.updateSetting('quickRateStep', `请输入按 [ 或 ] 键调整速度的步长 (0.1-${this.config.MAX_QUICK_RATE_STEP})`, this.config.MAX_QUICK_RATE_STEP));
            GM_registerMenuCommand('设置目标倍速调整步长', () => this.updateSetting('targetRateStep', `请输入按 +/- 键调整目标倍速的步长 (0.1-${this.config.MAX_RATE})`));

            // 如果当前网站是临时启用的,则提供"移除"选项
            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},请刷新页面`);
                    }
                });
            }
        }
        
        updateSetting(key, promptMessage, max = this.config.MAX_RATE) {
            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
                    showFloatingMessage(`设置失败: 请输入有效的值 (0.1-${max})`);
                }
            }
        }


        // 4. 初始化流程
        async initialize(isRetry = false) {
            this.cleanup();
        
            try {
                this.activeVideo = await this._findInitialVideo();
                console.log("初始化成功, 找到视频:", this.activeVideo);
        
                this._setupPersistentObservers();
                this.setupEventListeners();
                this.watchUrlChange();
        
            } catch (error) {
                console.warn("初始化尝试失败:", error.message);
                // 仅在首次尝试时启动重试逻辑
                if (!isRetry) {
                    // 如果是特定错误类型,比如找不到视频,则在一段时间后重试
                    if (error.type === "no_video" || error.type === "timeout") {
                        setTimeout(() => this.initialize(true).catch(console.error), this.config.INIT_RETRY_DELAY);
                    }
                }
                // 如果是重试失败,则不再继续,避免无限循环
            }
        }
        
        async _findInitialVideo() {
            try {
                // 尝试用快速方法找到视频
                const video = await this.waitForVideoElement();
                if (video) {
                    this.detectAndSetupVideos(); // 确保视频设置完成
                    return this.activeVideo || video;
                }
            } catch (error) {
                 // 如果快速方法超时或找不到,则尝试深度查找
                console.log("快速查找失败,尝试深度查找...");
                const deepVideos = this.deepFindVideoElements();
                if (deepVideos.length > 0) {
                    this.setupVideos(deepVideos);
                    showFloatingMessage(`通过深度查找发现了 ${deepVideos.length} 个视频`);
                    return deepVideos[0];
                }
            }
            
            // 如果所有方法都找不到视频,则抛出错误
            throw { type: "no_video", message: "在页面上找不到任何视频元素。" };
        }
        
        _setupPersistentObservers() {
            // 1. 合并 videoObserver 和 urlObserver, 并优化回调
            this.mainObserver = new MutationObserver((mutations) => {
                // 优先检查 URL 变化,因为它更轻量
                if (location.href !== this.currentUrl) {
                    this.handleUrlChange();
                    // URL 变化通常意味着页面重载或切换,此时可以先返回,等待 initialize
                    return;
                }

                // 检查 DOM 变动
                mutations.forEach(mutation => {
                    // 垃圾回收:检查是否有被管理的视频被移除了
                    mutation.removedNodes.forEach(removedNode => {
                        // 检查被移除的节点本身或者其子节点是否是我们正在管理的视频
                        const videosToRemove = [];
                        if (this.videoControlButtons.has(removedNode)) {
                            videosToRemove.push(removedNode);
                        } else if (removedNode.querySelectorAll) {
                            removedNode.querySelectorAll('video').forEach(video => {
                                if (this.videoControlButtons.has(video)) {
                                    videosToRemove.push(video);
                                }
                            });
                        }

                        videosToRemove.forEach(video => {
                             console.log("垃圾回收:清理被移除的视频", video);
                             const button = this.videoControlButtons.get(video);
                             if (button) button.remove();
                             this.videoControlButtons.delete(video);
                             if (this.activeVideo === video) {
                                 this.activeVideo = null;
                             }
                        });
                    });

                    // 检查是否有新视频被添加
                    const hasNewVideos = Array.from(mutation.addedNodes).some(n => n.tagName === 'VIDEO' || (n.querySelector && n.querySelector('video')));
                    if (hasNewVideos) {
                         console.log("侦测到新视频相关的DOM变动,调用防抖版检测...");
                         this.debouncedDetectAndSetupVideos();
                    }
                });
            });
            this.mainObserver.observe(document.body, { childList: true, subtree: true });
            this.activeObservers.add(this.mainObserver);

            // 2. 观察当前视频的父节点,以便在视频被替换时重新初始化 (保留)
            if (this.activeVideo && this.activeVideo.parentElement) {
                this.videoChangeObserver = new MutationObserver((mutations) => {
                    const videoWasRemoved = mutations.some(m => Array.from(m.removedNodes).some(n => n === this.activeVideo));
                    if (videoWasRemoved) {
                        console.log("侦测到当前活动视频节点被移除,将重新初始化...");
                        this.initialize().catch(console.error);
                    }
                });
                this.videoChangeObserver.observe(this.activeVideo.parentElement, { childList: true });
                this.activeObservers.add(this.videoChangeObserver);
            }
        }

        // 5. 清理与监听
        cleanup() {
            if (this.keydownListener) {
                window.removeEventListener("keydown", this.keydownListener, true);

                // 清理 iframe 内的监听器
                if (this.activeVideo) {
                    try {
                        const iframeWindow = this.activeVideo.ownerDocument.defaultView;
                        if (iframeWindow && iframeWindow !== window) {
                            iframeWindow.removeEventListener("keydown", this.keydownListener, true);
                        }
                    } catch(e) {
                        // iframe 可能已被销毁,忽略错误
                    }
                }

                this.keydownListener = null;
            }
            if (this.keyupListener) {
                window.removeEventListener("keyup", this.keyupListener, true);

                // 清理 iframe 内的监听器
                if (this.activeVideo) {
                    try {
                        const iframeWindow = this.activeVideo.ownerDocument.defaultView;
                        if (iframeWindow && iframeWindow !== window) {
                            iframeWindow.removeEventListener("keyup", this.keyupListener, true);
                        }
                    } catch(e) {
                        // iframe 可能已被销毁,忽略错误
                    }
                }

                this.keyupListener = null;
            }
            this.activeObservers.forEach(observer => observer.disconnect());
            this.activeObservers.clear();
            this.videoControlButtons.forEach(button => button.remove());
            this.videoControlButtons.clear();
            this.activeVideo = null;
        }

        handleUrlChange() {
            this.currentUrl = location.href;
            console.log("URL发生变化,重新初始化...");
            // 使用 setTimeout 延迟执行,确保新页面的 DOM 元素已加载
            setTimeout(() => this.initialize().catch(console.error), this.config.URL_CHANGE_INIT_DELAY);
        }

        watchUrlChange() {
            // MutationObserver 的部分已合并到 mainObserver 中
            // 这里只处理 History API 的监听

            const handleStateChange = this.handleUrlChange.bind(this);

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

            const originalReplaceState = history.replaceState;
            history.replaceState = function() {
                originalReplaceState.apply(this, arguments);
                handleStateChange();
            };
            
            window.addEventListener('popstate', handleStateChange);
        }


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

            // 如果视频在 iframe 中,也在 iframe 内设置监听
            if (this.activeVideo) {
                try {
                    const iframeWindow = this.activeVideo.ownerDocument.defaultView;
                    if (iframeWindow && iframeWindow !== window) {
                        iframeWindow.addEventListener("keydown", this.keydownListener, true);
                        iframeWindow.addEventListener("keyup", this.keyupListener, true);
                        console.log('✅ 已在 iframe 中设置键盘监听');
                    }
                } catch(e) {
                    console.warn('⚠️ 无法在 iframe 中设置监听器:', e.message);
                }
            }
        }

        // 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" });
                }, this.config.WAIT_FOR_VIDEO_TIMEOUT);
            });
        }
        
        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) {
                // 对于配置了特定规则的网站,进行主视频判断
                if (this.siteConfig.mainVideoSelector || this.siteConfig.mainVideoPredicate) {
                     if (!this.activeVideo || !videos.includes(this.activeVideo)) {
                        let mainVideo;
                        // 优先使用 predicate 函数判断
                        if (this.siteConfig.mainVideoPredicate) {
                             mainVideo = videos.find(this.siteConfig.mainVideoPredicate);
                        }
                        // 如果没有找到,再使用选择器判断
                        if (!mainVideo && this.siteConfig.mainVideoSelector) {
                            mainVideo = videos.find(v => v.matches(this.siteConfig.mainVideoSelector));
                        }
                         // 如果还是没有,则找一个未暂停的作为补充
                        if (!mainVideo) {
                             mainVideo = videos.find(v => !v.paused);
                        }

                        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) {
            // 检查是否已存在按钮,避免重复创建
            if (this.videoControlButtons.has(video)) {
                return;
            }

            // 创建圆形标签(24px,纯圆形无文字设计)
            const button = document.createElement('div');
            Object.assign(button.style, {
                position: 'absolute',
                top: '10px',
                left: '10px',
                width: '24px',
                height: '24px',
                borderRadius: '50%',
                background: 'rgba(255, 255, 255, 0.15)',
                border: '1px solid rgba(255, 255, 255, 0.3)',
                color: 'white',
                cursor: 'pointer',
                transition: 'all 0.2s ease',
                userSelect: 'none',
                zIndex: '9999'
            });

            // 悬停效果
            button.addEventListener('mouseenter', (e) => {
                // 如果已激活,不显示提示
                if (button.classList.contains('active')) {
                    return;
                }

                // 悬停时放大
                button.style.transform = 'scale(1.3)';
                button.style.background = 'rgba(255, 255, 255, 0.25)';
                button.style.borderColor = 'rgba(255, 255, 255, 0.5)';

                // 显示 tooltip
                this.showTooltip(button, '选择视频');
            });

            button.addEventListener('mouseleave', () => {
                if (!button.classList.contains('active')) {
                    button.style.transform = 'scale(1)';
                    button.style.background = 'rgba(255, 255, 255, 0.15)';
                    button.style.borderColor = 'rgba(255, 255, 255, 0.3)';
                }
            });

            // 点击切换事件
            button.addEventListener('click', (e) => {
                e.stopPropagation(); // 防止触发视频播放
                this.switchActiveVideo(video, button);
            });

            // 如果是当前活动视频,设为激活状态
            if (!this.activeVideo) {
                this.activeVideo = video;
                this.setActiveButton(button);
            } else if (video === this.activeVideo) {
                this.setActiveButton(button);
            }

            // 获取父容器
            const container = video.parentElement || document.body;
            const computedStyle = window.getComputedStyle(container);
            if (computedStyle.position === 'static') {
                container.style.position = 'relative';
            }

            // 添加到DOM
            container.appendChild(button);
            this.videoControlButtons.set(video, button);
        }

        /**
         * 显示 tooltip 提示
         * @param {HTMLElement} target 目标元素
         * @param {string} text 提示文字
         */
        showTooltip(target, text) {
            const tooltip = document.createElement('div');
            Object.assign(tooltip.style, {
                position: 'fixed',
                background: 'rgba(0, 0, 0, 0.9)',
                color: 'white',
                padding: '4px 8px',
                borderRadius: '4px',
                fontSize: '10px',
                whiteSpace: 'nowrap',
                pointerEvents: 'none',
                zIndex: '10000',
                opacity: '0',
                transition: 'opacity 0.2s ease',
                fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif'
            });
            tooltip.textContent = text;

            // 定位 tooltip
            document.body.appendChild(tooltip);
            const rect = target.getBoundingClientRect();

            // 计算位置
            let tooltipLeft = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2);
            let tooltipTop = rect.top - tooltip.offsetHeight - 8;

            // 边界检测
            if (tooltipLeft < 10) {
                tooltipLeft = 10;
            }
            if (tooltipLeft + tooltip.offsetWidth > window.innerWidth - 10) {
                tooltipLeft = window.innerWidth - tooltip.offsetWidth - 10;
            }
            if (tooltipTop < 10) {
                tooltipTop = rect.bottom + 8;
            }

            tooltip.style.left = tooltipLeft + 'px';
            tooltip.style.top = tooltipTop + 'px';

            // 显示
            setTimeout(() => tooltip.style.opacity = '1', 10);

            // 3秒后自动消失
            setTimeout(() => {
                tooltip.style.opacity = '0';
                setTimeout(() => tooltip.remove(), 200);
            }, 3000);
        }

        /**
         * 切换活动视频
         * @param {HTMLVideoElement} video 目标视频元素
         * @param {HTMLElement} button 按钮元素
         */
        switchActiveVideo(video, button) {
            // 重置所有按钮样式
            this.videoControlButtons.forEach((btn) => {
                this.resetButtonStyle(btn);
            });

            // 激活当前按钮
            this.setActiveButton(button);

            // 切换活动视频
            this.activeVideo = video;

            // 显示提示消息
            showFloatingMessage('已切换到该视频控制');
        }

        /**
         * 设置按钮为激活状态
         * @param {HTMLElement} button 按钮元素
         */
        setActiveButton(button) {
            button.classList.add('active');
            button.style.background = 'rgba(0, 128, 255, 0.3)';
            button.style.borderColor = 'rgba(0, 128, 255, 0.3)';
            button.style.boxShadow = '0 0 8px rgba(0, 128, 255, 0.3)';
            button.style.transform = 'scale(1)';

            // 添加中心小点指示器
            const dot = document.createElement('div');
            Object.assign(dot.style, {
                position: 'absolute',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                width: '4px',
                height: '4px',
                background: 'rgba(255, 255, 255, 0.5)',
                borderRadius: '50%',
                pointerEvents: 'none'
            });
            button.appendChild(dot);
        }

        /**
         * 重置按钮为默认状态
         * @param {HTMLElement} button 按钮元素
         */
        resetButtonStyle(button) {
            button.classList.remove('active');
            button.style.background = 'rgba(255, 255, 255, 0.15)';
            button.style.borderColor = 'rgba(255, 255, 255, 0.3)';
            button.style.boxShadow = 'none';
            button.style.transform = 'scale(1)';
            // 移除中心小点
            const dot = button.querySelector('div');
            if (dot) dot.remove();
        }

        // 8. 按键事件处理
        handleKeyDown(e) {
            // 忽略因长按而重复触发的 keydown 事件 (除了右箭头,它有自己的长按逻辑)
            if (e.repeat && e.code !== 'ArrowRight') {
                return;
            }
            
            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) {
            // 拦截空格键的 keyup 事件,防止冲突
            if (e.code === 'Space' && this.currentDomain.includes('youtube.com')) {
                 e.preventDefault();
                 e.stopImmediatePropagation();
            }

            if (e.code === 'ArrowRight') {
                // 防止重复处理 - 双重保险
                // 1. 事件对象标记
                if (e._videoControllerHandled) {
                    return;
                }
                e._videoControllerHandled = true;

                // 2. 全局标记
                if (this._rightKeyUpHandled) {
                    return;
                }
                this._rightKeyUpHandled = true;

                clearTimeout(this.rightKeyTimer);
                this.rightKeyTimer = null;

                if (this.downCount < this.config.SHORT_PRESS_MAX_COUNT) { //判定为短按
                    this.seek(this.config.SEEK_STEP_SECONDS);
                } else { //判定为长按
                    if(this.activeVideo) {
                        this.activeVideo.playbackRate = this.originalRate;
                        showFloatingMessage(`恢复播放速度: ${this.originalRate.toFixed(1)}x`);
                    }
                }
                this.downCount = 0;
            }
        }
        
        // 9. 按键处理器和具体功能实现
        _initializeKeyHandlers() {
            this.keyHandlers = {
                // 直接使用 .bind 将函数与参数关联,代码更紧凑
                'ArrowUp': this.adjustVolume.bind(this, this.config.VOLUME_STEP),
                'ArrowDown': this.adjustVolume.bind(this, -this.config.VOLUME_STEP),
                'Enter': this.toggleFullScreen.bind(this),
                'Space': this.togglePlayPause.bind(this),
                'ArrowLeft': this.seek.bind(this, -this.config.SEEK_STEP_SECONDS),
                'ArrowRight': this.handleRightArrowPress.bind(this), // 此函数逻辑复杂,保留原样
                'Equal': this.adjustTargetRate.bind(this, this.settings.targetRateStep),
                'Minus': this.adjustTargetRate.bind(this, -this.settings.targetRateStep),
                'BracketRight': this.adjustPlaybackRate.bind(this, this.settings.quickRateStep),
                'BracketLeft': this.adjustPlaybackRate.bind(this, -this.settings.quickRateStep),
                'KeyP': this.resetPlaybackRate.bind(this),
                'Comma': this.frameStep.bind(this, -1),
                'Period': this.frameStep.bind(this, 1),
            };
        }

        // 移除了 _handle... 系列的中间函数,因为它们已被 .bind 替代

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

        toggleFullScreen() {
            // 优先使用网站特定选择器
            if (this.siteConfig.fullscreenButtonSelector) {
                const fsButton = document.querySelector(this.siteConfig.fullscreenButtonSelector);
                if (fsButton) {
                    fsButton.click();
                    return;
                }
            }

            // 通用备用方案:使用原生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 ? '进' : '退'} ${this.config.SEEK_STEP_SECONDS} 秒`);
        }
        
        // 此方法逻辑复杂,保留原名,仅在 handler 中调用
        handleRightArrowPress() {
            if (this.activeVideo.paused) this.activeVideo.play();

            // 重置标记,允许新的KeyUp处理
            this._rightKeyUpHandled = false;

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

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

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

})();