Enhanced Video Speed Controller

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.

Install this script?
Author's suggested script

You may also like 视频倍速播放.

Install this script

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 or Violentmonkey 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         视频倍速播放增强版
// @name:en      Enhanced Video Speed Controller
// @namespace    http://tampermonkey.net/
// @version      1.5.6
// @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 的域名(包括嵌套 iframe)
         * @returns {Array<string>} 域名数组
         */
        detectCrossOriginIframeDomains() {
            const crossDomainIframes = new Set();

            const findIframes = (root, depth = 0) => {
                if (depth > 5) return;

                const iframes = root.querySelectorAll('iframe');
                iframes.forEach(iframe => {
                    try {
                        const src = iframe.src;
                        if (src) {
                            const url = new URL(src);
                            const domain = url.hostname;
                            if (domain && domain !== this.currentDomain) {
                                crossDomainIframes.add(domain);
                            }
                        }

                        // 尝试递归检测嵌套 iframe(仅限同源)
                        if (iframe.contentDocument) {
                            findIframes(iframe.contentDocument, depth + 1);
                        }
                    } catch (e) {
                        // 忽略跨域错误
                    }
                });
            };

            findIframes(document);
            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])];
                // 只有当有新的iframe域名时才更新
                if (combinedIframes.length > existingGroup.iframes.length) {
                    this.tempEnabledDomainGroups[existingIndex] = {
                        mainDomain,
                        iframes: combinedIframes,
                        createdAt: existingGroup.createdAt,
                        updatedAt: Date.now()
                    };
                    GM_setValue('tempEnabledDomainGroups', this.tempEnabledDomainGroups);
                }
            } 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;
                }
                // 检查当前域名是否在启用列表中
                const allDomains = this.getAllEnabledDomains();
                if (allDomains.includes(this.currentDomain)) {
                    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) {
                // 只在主页面且有 iframe 时静默退出
                if (window.self === window.top) {
                    const hasIframe = document.querySelector('iframe') !== null;
                    if (hasIframe) {
                        return;
                    }
                }

                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);
                    }
                } catch(e) {
                    // 忽略跨域错误
                }
            }
        }

        // 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();

})();