Greasy Fork

Greasy Fork is available in English.

滚动音量Dx版

新增自定义修饰键微调音量功能。滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)

当前为 2025-09-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         滾動音量Dx版 Scroll Volume Dx Edition
// @name:zh-CN   滚动音量Dx版
// @name:en      Scroll Volume Dx Edition
// @namespace    http://tampermonkey.net/
// @version      9.6.1
// @description  新增自訂修飾鍵微調音量功能。滾輪、013速度、28音量、46+-5sec、5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:zh-CN 新增自定义修饰键微调音量功能。滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:en  Added custom modifier key for fine volume adjustment. wheel scroll for volume. NumpadKey:013 for speed, 28 for volume, 46 for 5sec、5(space) for play/pause, enter for fullscreen, numpad+- for 5sec. Fully supports: YouTube, Bilibili, Steam. Bilibili live (partial)
// @match        *://www.youtube.com/*
// @match        *://www.bilibili.com/*
// @match        *://live.bilibili.com/*
// @match        *://www.twitch.tv/*
// @match        *://store.steampowered.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    const LANG = /^zh-(cn|tw|hk|mo|sg)/i.test(navigator.language) ? 'zh' : 'en';
    const i18n = {
        zh: {
            menuStep: '⚙️ 設定步進',
            menuLongStep: '⏱️ 設定長步進',
            menuVolumeStep: '🔊 設定音量步進',
            menuModifier: '🎚️ 設定修飾鍵微調',
            menuKeyFunc: '🎛️ 設定按鍵7/9功能',
            promptStep: '設定快進/快退 (秒)',
            promptLongStep: '設定長跳轉 (秒)',
            promptVolume: '設定音量幅度 (%)',
            modifierOptions: {
                1: '1. Alt 鍵',
                2: '2. Ctrl 鍵',
                3: '3. Shift 鍵',
                4: '4. Meta 鍵 (⌘)',
                5: '5. 關閉此功能'
            },
            keyFuncOptions: {
                1: '1. 長步進',
                2: '2. 上一頁/下一頁',
                3: '3. 上/下一個影片',
                4: '4. 平台原生功能'
            },
            saveAlert: '設定已保存,需重新整理頁面後生效'
        },
        en: {
            menuStep: '⚙️ Set Step',
            menuLongStep: '⏱️ Set Long Step',
            menuVolumeStep: '🔊 Set Volume Step',
            menuModifier: '🎚️ Set Modifier Key',
            menuKeyFunc: '🎛️ Set Key 7/9 Function',
            promptStep: 'Set step time (seconds)',
            promptLongStep: 'Set long jump time (seconds)',
            promptVolume: 'Set volume step (%)',
            modifierOptions: {
                1: '1. Alt key',
                2: '2. Ctrl key',
                3: '3. Shift key',
                4: '4. Meta key (⌘)',
                5: '5. Disable feature'
            },
            keyFuncOptions: {
                1: '1. Long step',
                2: '2. Browser navigation',
                3: '3. Previous/Next video',
                4: '4. Platform native'
            },
            saveAlert: 'Settings saved. Refresh page to apply'
        }
    };

    // 配置菜单本地化
    const registerMenuCommands = () => {
        const t = i18n[LANG];
        GM_registerMenuCommand(t.menuStep, () => handleConfigPrompt(t.promptStep, 'stepTime'));
        GM_registerMenuCommand(t.menuLongStep, () => handleConfigPrompt(t.promptLongStep, 'stepTimeLong'));
        GM_registerMenuCommand(t.menuVolumeStep, () => handleConfigPrompt(t.promptVolume, 'stepVolume'));
        GM_registerMenuCommand(t.menuModifier, handleModifierSetting);
        GM_registerMenuCommand(t.menuKeyFunc, handleKeyFunctionSetting);
    };

    const handleConfigPrompt = (promptText, configKey) => {
        const newVal = prompt(promptText, CONFIG[configKey]);
        if (newVal && !isNaN(newVal)) {
            CONFIG[configKey] = parseFloat(newVal);
            saveConfig(CONFIG);
        }
    };

    const handleModifierSetting = () => {
        const t = i18n[LANG];
        const options = t.modifierOptions;
        const choice = prompt(
            `${LANG === 'zh' ? '選擇音量微調修飾鍵:' : 'Select modifier key:'}\n${Object.values(options).join('\n')}`,
            CONFIG.modifierKey
        );
        if (choice && options[choice]) {
            CONFIG.modifierKey = parseInt(choice);
            saveConfig(CONFIG);
            alert(t.saveAlert);
        }
    };

    const handleKeyFunctionSetting = () => {
        const t = i18n[LANG];
        const baseOptions = {...t.keyFuncOptions};
        if (!['YOUTUBE', 'BILIBILI'].includes(PLATFORM)) delete baseOptions[4];

        const getChoice = (msgKey, currentVal) => {
            const message = `${LANG === 'zh' ? '選擇按鍵功能:' : 'Select key function:'}\n${Object.values(baseOptions).join('\n')}`;
            return prompt(message, currentVal);
        };

        const choice7 = getChoice('key7', CONFIG.key7Function);
        if (choice7 && baseOptions[choice7]) CONFIG.key7Function = parseInt(choice7);

        const choice9 = getChoice('key9', CONFIG.key9Function);
        if (choice9 && baseOptions[choice9]) CONFIG.key9Function = parseInt(choice9);

        saveConfig(CONFIG);
    };

    // 获取标准化的域名标识 (简化为二级域名)
    const getDomainId = () => {
        const hostParts = location.hostname.split('.');
        return hostParts.length > 2 ? hostParts.slice(-2).join('_') : hostParts.join('_');
    };

    // 平台检测
    const PLATFORM = (() => {
        const host = location.hostname;
        if (/youtube\.com|youtu\.be/.test(host)) return "YOUTUBE";
        if (/www.bilibili\.com/.test(host)) return "BILIBILI";
        if (/twitch\.tv/.test(host)) return "TWITCH";
        if (/steam(community|powered)\.com/.test(host)) return "STEAM";
        return "GENERIC";
    })();

    // 存储配置结构优化
    const CONFIG_STORAGE_KEY = 'ScrollVolumeDxConfig';
    const DEFAULT_CONFIG = {
        stepTime: 5,
        stepTimeLong: 30,
        stepVolume: 10,
        key7Function: ['YOUTUBE', 'BILIBILI'].includes(PLATFORM) ? 4 : 1,
        key9Function: ['YOUTUBE', 'BILIBILI'].includes(PLATFORM) ? 4 : 1,
        modifierKey: 5, // 新增:1=Alt 2=Ctrl 3=Shift 4=Meta 5=None
        fineVolumeStep: 1 // 微调音量步进值
    };

    // 获取配置(修复自定义参数保存问题)
    const getConfig = () => {
        const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {});
        const domainId = getDomainId();

        // 返回深拷贝的配置对象
        return {
            ...DEFAULT_CONFIG,
            ...(savedConfig[domainId] || {})
        };
    };

    // 保存配置(修复自定义参数保存问题)
    const saveConfig = (config) => {
        const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {});
        const domainId = getDomainId();

        // 创建当前域名的配置副本
        const currentConfig = { ...config };

        // 检查是否所有值都是默认值
        const isDefault = Object.keys(DEFAULT_CONFIG).every(key =>
            currentConfig[key] === DEFAULT_CONFIG[key]
        );

        if (isDefault) {
            // 删除默认配置
            if (savedConfig[domainId]) {
                delete savedConfig[domainId];
                GM_setValue(CONFIG_STORAGE_KEY, savedConfig);
            }
            return;
        }

        // 只存储与默认值不同的配置项
        const diffConfig = {};
        Object.keys(currentConfig).forEach(key => {
            if (currentConfig[key] !== DEFAULT_CONFIG[key]) {
                diffConfig[key] = currentConfig[key];
            }
        });

        savedConfig[domainId] = diffConfig;
        GM_setValue(CONFIG_STORAGE_KEY, savedConfig);
    };

    // 初始化配置
    const CONFIG = (() => {
        const config = getConfig();
        saveConfig(config);
        return config;
    })();

    registerMenuCommands();
    let cachedVideo = null;
    let lastVideoCheck = 0;
    let videoElements = [];
    let currentVideoIndex = 0;
    let activeVideoId = null;

    const videoStateMap = new WeakMap();

    function getVideoState(video) {
        if (!videoStateMap.has(video)) {
            videoStateMap.set(video, {
                lastCustomRate: 1.0,
                isDefaultRate: true
            });
        }
        return videoStateMap.get(video);
    }

    // 生成视频唯一ID
    const generateVideoId = (video) =>
        `${video.src}_${video.clientWidth}x${video.clientHeight}`;

    function getVideoElement() {
        // 优先使用当前激活的视频
        if (activeVideoId) {
            const activeVideo = videoElements.find(v => generateVideoId(v) === activeVideoId);
            if (activeVideo && document.contains(activeVideo)) {
                cachedVideo = activeVideo;
                return cachedVideo;
            }
        }

        // 常规检测逻辑
        if (cachedVideo && document.contains(cachedVideo) && (Date.now() - lastVideoCheck < 300)) {
            return cachedVideo;
        }

        const handler = PLATFORM_HANDLERS[PLATFORM] || PLATFORM_HANDLERS.GENERIC;
        cachedVideo = handler.getVideo();
        lastVideoCheck = Date.now();

        // 更新视频元素列表和当前索引
        updateVideoElements();
        if (cachedVideo && videoElements.length > 0) {
            currentVideoIndex = videoElements.indexOf(cachedVideo);
            if (currentVideoIndex === -1) currentVideoIndex = 0;
            activeVideoId = generateVideoId(cachedVideo);
        }

        return cachedVideo;
    }

    function updateVideoElements() {
        videoElements = Array.from(document.querySelectorAll('video'))
            .filter(v => v.offsetParent !== null && v.readyState > 0);
    }

    function switchToNextVideo() {
        if (videoElements.length < 2) return null;

        currentVideoIndex = (currentVideoIndex + 1) % videoElements.length;
        cachedVideo = videoElements[currentVideoIndex];
        activeVideoId = generateVideoId(cachedVideo);
        lastVideoCheck = Date.now();

        return cachedVideo;
    }

    function switchToPrevVideo() {
        if (videoElements.length < 2) return null;

        currentVideoIndex = (currentVideoIndex - 1 + videoElements.length) % videoElements.length;
        cachedVideo = videoElements[currentVideoIndex];
        activeVideoId = generateVideoId(cachedVideo);
        lastVideoCheck = Date.now();

        return cachedVideo;
    }

    // 修正通用音量調整函數
    function commonAdjustVolume(video, delta) {
        const isFineAdjust = Math.abs(delta) === CONFIG.fineVolumeStep;
        const actualDelta = isFineAdjust ? delta : (delta > 0 ? CONFIG.stepVolume : -CONFIG.stepVolume); // 修正符號

        const newVolume = clampVolume((video.volume * 100) + actualDelta);
        video.volume = newVolume / 100;
        showVolume(newVolume);
        return newVolume;
    }

    function clampVolume(vol) {
        return Math.round(Math.max(0, Math.min(100, vol)) * 100) / 100;
    }

    const PLATFORM_HANDLERS = {
        YOUTUBE: {
            getVideo: () => document.querySelector('video, ytd-player video') || findVideoInIframes(),
            adjustVolume: (video, delta) => {
                const ytPlayer = document.querySelector('#movie_player');
                if (ytPlayer?.getVolume) {
                    const currentVol = ytPlayer.getVolume();
                    const newVol = clampVolume(currentVol + delta); // 直接應用delta
                    ytPlayer.setVolume(newVol);
                    video.volume = newVol / 100;
                    showVolume(newVol);
                } else {
                    commonAdjustVolume(video, delta); // 後備通用邏輯
                }
            },
            toggleFullscreen: () => document.querySelector('.ytp-fullscreen-button')?.click(),
            specialKeys: {
                'Space': () => {},
                'Numpad7': () => document.querySelector('.ytp-prev-button')?.click(),
                'Numpad9': () => document.querySelector('.ytp-next-button')?.click()
            }
        },
        BILIBILI: {
            getVideo: () => document.querySelector('.bpx-player-video-wrap video') || findVideoInIframes(),
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: () => document.querySelector('.bpx-player-ctrl-full')?.click(),
            specialKeys: {
                'Space': () => {},
                'Numpad2': () => {},
                'Numpad8': () => {},
                'Numpad4': () => {},
                'Numpad6': () => {},
                'Numpad7': () => document.querySelector('.bpx-player-ctrl-prev')?.click(),
                'Numpad9': () => document.querySelector('.bpx-player-ctrl-next')?.click()
            }
        },
        TWITCH: {
            getVideo: () => document.querySelector('.video-ref video') || findVideoInIframes(),
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: () => document.querySelector('[data-a-target="player-fullscreen-button"]')?.click(),
            specialKeys: {
                'Numpad7': () => simulateKeyPress('ArrowLeft'),
                'Numpad9': () => simulateKeyPress('ArrowRight')
            }
        },
        STEAM: {
            getVideo: () => {
                const videos = Array.from(document.querySelectorAll('video'));
                const playingVideo = videos.find(v => v.offsetParent !== null && !v.paused);
                if (playingVideo) return playingVideo;
                const visibleVideo = videos.find(v => v.offsetParent !== null);
                if (visibleVideo) return visibleVideo;
                return findVideoInIframes();
            },
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video) => {
                if (!video) return;
                const container = video.closest('.game_hover_activated') || video.parentElement;
                if (container && !document.fullscreenElement) {
                    container.requestFullscreen?.().catch(() => video.requestFullscreen?.());
                } else {
                    document.exitFullscreen?.();
                }
            },
            handleWheel: function(e) {
                if (isInputElement(e.target)) return;
                const video = this.getVideo();
                if (!video) return;

                const rect = video.getBoundingClientRect();
                const inVideoArea =
                      e.clientX >= rect.left - 50 && e.clientX <= rect.right + 50 &&
                      e.clientY >= rect.top - 30 && e.clientY <= rect.bottom + 30;

                if (inVideoArea) {
                    e.preventDefault();
                    e.stopPropagation();
                    const delta = -Math.sign(e.deltaY);
                    this.adjustVolume(video, delta * CONFIG.stepVolume);
                    showVolume(video.volume * 100);
                }
            }
        },
        GENERIC: {
            getVideo: () => {
                return document.querySelector('video.player') ||
                       findVideoInIframes() ||
                       document.querySelector('video, .video-player video, .video-js video, .player-container video');
            },
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video) => toggleNativeFullscreen(video),
        }
    };

    function findVideoInIframes() {
        const iframes = document.querySelectorAll('iframe');
        for (const iframe of iframes) {
            try {
                const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
                return iframeDoc?.querySelector('video');
            } catch {}
        }
        return null;
    }

    function toggleNativeFullscreen(video) {
        if (!video) return;
        try {
            if (document.fullscreenElement) {
                document.exitFullscreen();
            } else {
                let elementToFullscreen = video;
                for (let i = 0; i < 2; i++) {
                    elementToFullscreen = elementToFullscreen.parentElement || elementToFullscreen;
                }
                elementToFullscreen.requestFullscreen?.() ||
                elementToFullscreen.webkitRequestFullscreen?.() ||
                elementToFullscreen.msRequestFullscreen?.() ||
                video.requestFullscreen?.() ||
                video.webkitRequestFullscreen?.() ||
                video.msRequestFullscreen?.();
            }
        } catch (e) {
            console.error('Fullscreen error:', e);
        }
    }

    function simulateKeyPress(key) {
        document.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true}));
    }

    function isInputElement(target) {
        return /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || target.isContentEditable;
    }

    function adjustRate(video, changeValue) {
        const state = getVideoState(video);
        const newRate = Math.max(0.1, Math.min(16, video.playbackRate + changeValue));
        video.playbackRate = parseFloat(newRate.toFixed(1));
        state.lastCustomRate = video.playbackRate;
        state.isDefaultRate = (video.playbackRate === 1.0);
        showVolume(video.playbackRate * 100);
    }

    function togglePlaybackRate(video) {
        const state = getVideoState(video);
        if (state.isDefaultRate) {
            video.playbackRate = state.lastCustomRate;
            state.isDefaultRate = false;
        } else {
            state.lastCustomRate = video.playbackRate;
            video.playbackRate = 1.0;
            state.isDefaultRate = true;
        }
        showVolume(video.playbackRate * 100);
    }

    function showVolume(vol) {
        const display = document.getElementById('dynamic-volume-display') || createVolumeDisplay();
        display.textContent = `${Math.round(vol)}%`;
        display.style.opacity = '1';
        setTimeout(() => display.style.opacity = '0', 1000);
    }

    function createVolumeDisplay() {
        const display = document.createElement('div');
        display.id = 'dynamic-volume-display';
        Object.assign(display.style, {
            position: 'fixed',
            zIndex: 2147483647,
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            padding: '10px 20px',
            borderRadius: '8px',
            backgroundColor: 'rgba(0, 0, 0, 0.7)',
            color: '#fff',
            fontSize: '24px',
            fontFamily: 'Arial, sans-serif',
            opacity: '0',
            transition: 'opacity 1s',
            pointerEvents: 'none'
        });
        document.body.appendChild(display);
        return display;
    }

    function handleVideoWheel(e) {
        e.preventDefault();
        e.stopPropagation();
        const video = e.target;
        const normalizedDelta = -Math.sign(e.deltaY); // 滾輪向上=+1,向下=-1
        PLATFORM_HANDLERS[PLATFORM].adjustVolume(video, normalizedDelta * CONFIG.stepVolume);
    }

function handleTwitchWheel(e) {
    if (isInputElement(e.target)) return;
    const video = getVideoElement();
    if (!video) return;

    const rect = video.getBoundingClientRect();
    const inVideoArea =
        e.clientX >= rect.left - 50 && e.clientX <= rect.right + 50 &&
        e.clientY >= rect.top - 30 && e.clientY <= rect.bottom + 30;

    if (inVideoArea) {
        e.preventDefault();
        e.stopPropagation();
        const delta = -Math.sign(e.deltaY);
        const volumeChange = delta * CONFIG.stepVolume;
        PLATFORM_HANDLERS.TWITCH.adjustVolume(video, volumeChange);
        showVolume(video.volume * 100);
    }
}


    function handleKeyEvent(e) {
        if (isInputElement(e.target)) return;
        const video = getVideoElement();
        const handler = PLATFORM_HANDLERS[PLATFORM];

        // 強化修飾鍵檢測邏輯
        const isCustomModifier = (() => {
            if (CONFIG.modifierKey === 5) return false;

            const requiredModifier = {
                1: 'altKey',
                2: 'ctrlKey',
                3: 'shiftKey',
                4: 'metaKey'
            }[CONFIG.modifierKey];

            // 嚴格檢測:僅允許單一修飾鍵且無其他按鍵組合
            const otherModifiers = ['altKey','ctrlKey','shiftKey','metaKey']
            .filter(k => k !== requiredModifier)
            .some(k => e[k]);

            return e[requiredModifier] && !otherModifiers;
        })();

        // ==== 修正點2:非自定義修飾鍵穿透處理 ====
        const hasOtherModifiers = e.altKey || e.ctrlKey || e.shiftKey || e.metaKey;
        if (!isCustomModifier && hasOtherModifiers) {
            return; // 允許瀏覽器處理其他修飾鍵組合
        }

        // ==== 修正點3:微調音量步進值應用 ====
        if (isCustomModifier) {
            const volumeActions = {
                'Numpad8': () => handler.adjustVolume(video, CONFIG.fineVolumeStep),
                'Numpad2': () => handler.adjustVolume(video, -CONFIG.fineVolumeStep)
            };
            if (volumeActions[e.code]) {
                volumeActions[e.code]();
                e.preventDefault();
                return;
            }
            return;
        }

        // 处理按键7
        if (e.code === 'Numpad7') {
            switch (CONFIG.key7Function) {
                case 1: // 长步进
                    video && (video.currentTime -= CONFIG.stepTimeLong);
                    break;
                case 2: // 浏览器返回
                    history.back();
                    break;
                case 3: // 上一个影片
                    switchToPrevVideo()?.play().catch(() => {});
                    break;
                case 4: // 平台原生功能
                    if (handler.specialKeys?.Numpad7) {
                        handler.specialKeys.Numpad7();
                    }
                    break;
            }
            e.preventDefault();
            return;
        }

        // 处理按键9
        if (e.code === 'Numpad9') {
            switch (CONFIG.key9Function) {
                case 1: // 长步进
                    video && (video.currentTime += CONFIG.stepTimeLong);
                    break;
                case 2: // 浏览器前进
                    history.forward();
                    break;
                case 3: // 下一个影片
                    switchToNextVideo()?.play().catch(() => {});
                    break;
                case 4: // 平台原生功能
                    if (handler.specialKeys?.Numpad9) {
                        handler.specialKeys.Numpad9();
                    }
                    break;
            }
            e.preventDefault();
            return;
        }

        // 处理平台特殊按键
        if (handler.specialKeys?.[e.code]) {
            handler.specialKeys[e.code]();
            e.preventDefault();
            return;
        }

        // 处理其他通用按键
        const actions = {
            'Space': () => video && video[video.paused ? 'play' : 'pause'](),
            'Numpad5': () => video && video[video.paused ? 'play' : 'pause'](),
            'NumpadEnter': () => handler.toggleFullscreen(video),
            'NumpadAdd': () => video && (video.currentTime += video.duration * 0.1),
            'NumpadSubtract': () => video && (video.currentTime -= video.duration * 0.1),
            'Numpad0': () => video && togglePlaybackRate(video),
            'Numpad1': () => video && adjustRate(video, -0.1),
            'Numpad3': () => video && adjustRate(video, 0.1),
            'Numpad8': () => video && handler.adjustVolume(video, CONFIG.stepVolume),
            'Numpad2': () => video && handler.adjustVolume(video, -CONFIG.stepVolume),
            'Numpad4': () => video && (video.currentTime -= CONFIG.stepTime),
            'Numpad6': () => video && (video.currentTime += CONFIG.stepTime)
        };

        if (actions[e.code]) {
            actions[e.code]();
            e.preventDefault();
        }
    }

    function bindVideoEvents() {
        if (PLATFORM === 'TWITCH' || PLATFORM === 'STEAM') return;

        document.querySelectorAll('video').forEach(video => {
            if (!video.dataset.volumeBound) {
                video.addEventListener('wheel', handleVideoWheel, { passive: false });
                video.dataset.volumeBound = 'true';
            }
        });
    }

    function init() {
        bindVideoEvents();
        document.addEventListener('keydown', handleKeyEvent, true);
        if (PLATFORM === 'STEAM') {
            document.addEventListener('wheel',
           PLATFORM_HANDLERS.STEAM.handleWheel.bind(PLATFORM_HANDLERS.STEAM),
           { capture: true, passive: false }
        );
        }
        if (PLATFORM === 'TWITCH') {
            document.addEventListener('wheel', handleTwitchWheel, { capture: true, passive: false });
        }

        // 初始化视频元素列表
        updateVideoElements();

        // 监听DOM变化更新视频列表
        new MutationObserver(() => {
            bindVideoEvents();
            updateVideoElements();
            if (activeVideoId && !videoElements.some(v => generateVideoId(v) === activeVideoId)) {
                activeVideoId = null;
            }
        }).observe(document.body, { childList: true, subtree: true });
    }

    if (document.readyState !== 'loading') init();
    else document.addEventListener('DOMContentLoaded', init);
})();