Greasy Fork

来自缓存

Greasy Fork is available in English.

视频临时倍速+B站字幕开关记忆

视频播放增强:1. 长按左键临时加速 2. B站字幕开关记忆 3. 支持更多视频播放器

当前为 2025-10-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         视频临时倍速+B站字幕开关记忆
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  视频播放增强:1. 长按左键临时加速 2. B站字幕开关记忆 3. 支持更多视频播放器
// @author       Alonewinds
// @match        *://*/*
// @exclude      *://*/iframe/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @license      MIT
// @icon         https://s1.aigei.com/src/img/png/a6/a6c975c4efb84ebea1126c902f7daf1f.png?e=2051020800&token=P7S2Xpzfz11vAkASLTkfHN7Fw-oOZBecqeJaxypL:t5hcie9Hw5PjZfuwchVYoN5lrlo=
// ==/UserScript==

(function() {
    'use strict';

    if (window.location.hostname.includes('bilibili.com') &&
        window.self !== window.top &&
        window.location.hostname !== 'player.bilibili.com') {
        return;
    }

    // 默认配置
    const config = {
        speedRate: GM_getValue('speedRate', 2.0),
        minPressTime: 200,
        selectors: {
            'www.bilibili.com': '.bpx-player-video-area',
            'www.youtube.com': '.html5-video-player',
            'default': '.video-controls, .progress-bar, [role="slider"]'
        },
        debug: false
    };

    // 字幕相关常量选择器
    const SUBTITLE_SELECTORS = {
        VIDEO_WRAP: '.bpx-player-video-wrap',
        VIDEO: 'video',
        SUBTITLE_BUTTON: '.bpx-player-ctrl-subtitle-result',
        SUBTITLE_TOGGLE: '.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle',
        CHINESE_LANGUAGE_OPTION: '.bpx-player-ctrl-subtitle-language-item[data-lan="ai-zh"]',
        ACTIVE_CHINESE_LANGUAGE: '.bpx-player-ctrl-subtitle-language-item.bpx-state-active[data-lan="ai-zh"]',
        OFF_SUBTITLE_OPTION: '.bpx-player-ctrl-subtitle-language-item[data-lan="off"]',
        MAX_RETRIES: 5
    };

    const TIMING = {
        INITIAL_SUBTITLE_DELAY: 2000,
        SUBTITLE_CHECK_INTERVAL: 500,
        LANGUAGE_CLICK_DELAY: 100
    };

    // 状态变量
    let pressStartTime = 0;
    let originalSpeed = 1.0;
    let isPressed = false;
    let activeVideo = null;
    let isLongPress = false;
    let preventNextClick = false;

    // B站字幕相关变量
    let subtitleCheckTimer = null;
    let animationFrameId = null;
    let urlObserver = null;
    let isAutoSetting = false; // 标记是否正在自动设置字幕

    // 调试日志函数
    function debugLog(...args) {
        if (config.debug) {
            console.log(...args);
        }
    }

    // ================ 字幕功能 ================

    // 获取全局字幕状态
    function getGlobalSubtitleState() {
        return GM_getValue('globalSubtitleState', true); // 默认开启字幕
    }

    // 保存全局字幕状态
    function saveGlobalSubtitleState(isOpen) {
        GM_setValue('globalSubtitleState', isOpen);
        debugLog('保存字幕状态:', isOpen);
    }

    // 检测字幕是否开启
    function isSubtitleOn() {
        // 方法1:检查激活的中文字幕选项
        const activeLanguageItem = document.querySelector(SUBTITLE_SELECTORS.ACTIVE_CHINESE_LANGUAGE);
        if (activeLanguageItem) {
            return true;
        }

        // 方法2:检查字幕按钮状态
        const subtitleBtn = document.querySelector(SUBTITLE_SELECTORS.SUBTITLE_TOGGLE);
        if (subtitleBtn && subtitleBtn.classList.contains('bpx-state-active')) {
            return true;
        }

        // 方法3:检查是否有可用的中文字幕选项
        const chineseOption = document.querySelector(SUBTITLE_SELECTORS.CHINESE_LANGUAGE_OPTION);
        return !chineseOption; // 如果没有中文字幕选项,说明字幕已开启
    }

    // 设置字幕状态
    function setSubtitleState(desiredState) {
        if (isAutoSetting) return;

        isAutoSetting = true;
        let retryCount = 0;

        const intervalId = setInterval(() => {
            if (retryCount >= SUBTITLE_SELECTORS.MAX_RETRIES) {
                clearInterval(intervalId);
                isAutoSetting = false;
                return;
            }
            retryCount++;

            const subtitleToggle = document.querySelector(SUBTITLE_SELECTORS.SUBTITLE_TOGGLE);
            if (!subtitleToggle) return;

            clearInterval(intervalId);

            // 检查当前状态
            const currentState = isSubtitleOn();
            if (currentState === desiredState) {
                isAutoSetting = false;
                return;
            }

            // 打开字幕面板
            subtitleToggle.click();

            setTimeout(() => {
                if (desiredState) {
                    // 开启字幕
                    const chineseOption = document.querySelector(SUBTITLE_SELECTORS.CHINESE_LANGUAGE_OPTION);
                    if (chineseOption) {
                        chineseOption.click();
                        debugLog('自动开启字幕');
                    }
                } else {
                    // 关闭字幕
                    const offOption = document.querySelector(SUBTITLE_SELECTORS.OFF_SUBTITLE_OPTION);
                    if (offOption) {
                        offOption.click();
                        debugLog('自动关闭字幕');
                    }
                }

                setTimeout(() => {
                    isAutoSetting = false;
                }, 500);

            }, TIMING.LANGUAGE_CLICK_DELAY);

        }, TIMING.SUBTITLE_CHECK_INTERVAL);
    }

    // 初始化字幕功能
    function initSubtitleAutoOpen() {
        // 初始检查视频元素
        checkAndInitVideoListener();

        // 监听页面变化,处理视频切换场景
        const observer = new MutationObserver(() => {
            checkAndInitVideoListener();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 设置字幕按钮点击监听
        setupSubtitleButtonListener();
    }

    // 检查并初始化视频监听器
    function checkAndInitVideoListener() {
        const videoWrapElement = document.querySelector(SUBTITLE_SELECTORS.VIDEO_WRAP);
        if (!videoWrapElement) return;

        const videoElement = videoWrapElement.querySelector(SUBTITLE_SELECTORS.VIDEO);
        if (!videoElement) return;

        // 移除已存在的事件监听,避免重复绑定
        videoElement.removeEventListener('loadeddata', onVideoLoaded);
        videoElement.addEventListener('loadeddata', onVideoLoaded);
    }

    // 视频加载完成处理函数
    function onVideoLoaded() {
        setTimeout(applySubtitleMemory, TIMING.INITIAL_SUBTITLE_DELAY);
    }

    // 应用记忆的字幕状态
    function applySubtitleMemory() {
        const rememberedState = getGlobalSubtitleState();
        setSubtitleState(rememberedState);
    }

    // 设置字幕按钮点击监听
    function setupSubtitleButtonListener() {
        // 使用MutationObserver监听字幕相关元素的变化
        const subtitleObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === 1) { // Element node
                            // 检查是否是字幕面板
                            if (node.classList && (
                                node.classList.contains('bpx-player-ctrl-subtitle-panel') ||
                                node.querySelector('.bpx-player-ctrl-subtitle-panel')
                            )) {
                                // 字幕面板出现,设置选项点击监听
                                setTimeout(() => {
                                    setupSubtitleOptionListeners();
                                }, 100);
                            }
                        }
                    });
                }
            });
        });

        subtitleObserver.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 直接监听字幕按钮点击
        document.addEventListener('click', (e) => {
            const subtitleToggle = e.target.closest(SUBTITLE_SELECTORS.SUBTITLE_TOGGLE);
            if (subtitleToggle && !isAutoSetting) {
                // 用户点击了字幕按钮,延迟检查状态变化
                setTimeout(() => {
                    const currentState = isSubtitleOn();
                    saveGlobalSubtitleState(currentState);
                    debugLog('用户点击字幕按钮,保存状态:', currentState);
                }, 1000);
            }
        }, true);
    }

    // 设置字幕选项点击监听
    function setupSubtitleOptionListeners() {
        const subtitleOptions = document.querySelectorAll([
            SUBTITLE_SELECTORS.CHINESE_LANGUAGE_OPTION,
            SUBTITLE_SELECTORS.OFF_SUBTITLE_OPTION
        ].join(','));

        subtitleOptions.forEach(option => {
            if (!option._hasListener) {
                option._hasListener = true;
                option.addEventListener('click', () => {
                    if (isAutoSetting) return;

                    // 用户点击了字幕选项,保存状态
                    setTimeout(() => {
                        const currentState = isSubtitleOn();
                        saveGlobalSubtitleState(currentState);
                        debugLog('用户选择字幕选项,保存状态:', currentState);
                    }, 500);
                });
            }
        });
    }

    // ================ 倍速控制功能 ================
    function findVideoElement(element) {
        if (!element) return null;
        if (element instanceof HTMLVideoElement) return element;

        const domain = window.location.hostname;
        if (domain === 'www.bilibili.com') {
            const playerArea = document.querySelector('.bpx-player-video-area');
            if (!playerArea?.contains(element)) return null;
        } else if (domain === 'www.youtube.com') {
            const ytPlayer = element.closest('.html5-video-player');
            if (!ytPlayer?.contains(element)) return null;
            const video = ytPlayer.querySelector('video');
            if (video) return video;
        }

        const controlSelector = config.selectors.default;
        if (element.closest(controlSelector)) return null;

        const container = element.closest('*:has(video)');
        const video = container?.querySelector('video');
        return video && window.getComputedStyle(video).display !== 'none' ? video : null;
    }

    function setYouTubeSpeed(video, speed) {
        if (window.location.hostname === 'www.youtube.com') {
            const player = video.closest('.html5-video-player');
            if (player) {
                try {
                    if (player._speedInterval) {
                        clearInterval(player._speedInterval);
                        player._speedInterval = null;
                    }
                    video.playbackRate = speed;
                    if (speed !== 1.0) {
                        player._speedInterval = setInterval(() => {
                            if (video.playbackRate !== speed) {
                                video.playbackRate = speed;
                            }
                        }, 100);
                        setTimeout(() => {
                            if (player._speedInterval) {
                                clearInterval(player._speedInterval);
                                player._speedInterval = null;
                            }
                        }, 5000);
                    }
                    video.dispatchEvent(new Event('ratechange'));
                } catch (e) {
                    console.error('设置 YouTube 播放速度失败:', e);
                }
            }
        } else {
            video.playbackRate = speed;
        }
    }

    function startPressDetection() {
        if (!animationFrameId) {
            function checkPress() {
                handlePressDetection();
                animationFrameId = requestAnimationFrame(checkPress);
            }
            checkPress();
        }
    }

    function stopPressDetection() {
        if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
            animationFrameId = null;
        }
    }

    function handleMouseDown(e) {
        if (e.button !== 0) return;
        const domain = window.location.hostname;
        let video = null;
        let playerArea = null;

        if (domain === 'www.bilibili.com' || domain === 'www.youtube.com') {
            const selector = config.selectors[domain];
            playerArea = document.querySelector(selector);
            if (!playerArea?.contains(e.target)) return;
            video = findVideoElement(e.target);
        } else {
            video = findVideoElement(e.target);
            if (video) {
                playerArea = video.closest('*:has(video)') || video.parentElement;
                if (!playerArea?.contains(e.target)) return;
            }
        }

        if (!video || video.paused) {
            hideSpeedIndicator();
            return;
        }

        pressStartTime = Date.now();
        activeVideo = video;
        originalSpeed = video.playbackRate;
        isPressed = true;
        isLongPress = false;
        preventNextClick = false;
        startPressDetection();
    }

    function handleMouseUp(e) {
        if (!isPressed || !activeVideo) return;
        const pressDuration = Date.now() - pressStartTime;
        if (pressDuration >= config.minPressTime) {
            preventNextClick = true;
            setYouTubeSpeed(activeVideo, originalSpeed);
            hideSpeedIndicator();
        }
        isPressed = false;
        isLongPress = false;
        activeVideo = null;
        stopPressDetection();
    }

    function handlePressDetection() {
        if (!isPressed || !activeVideo) return;
        const pressDuration = Date.now() - pressStartTime;
        if (pressDuration >= config.minPressTime) {
            const currentSpeedRate = GM_getValue('speedRate', config.speedRate);
            if (activeVideo.playbackRate !== currentSpeedRate) {
                setYouTubeSpeed(activeVideo, currentSpeedRate);
            }
            if (!isLongPress) {
                isLongPress = true;
                const playerArea = activeVideo.closest('*:has(video)') || activeVideo.parentElement;
                let indicator = document.querySelector('.speed-indicator');
                if (!indicator) {
                    indicator = document.createElement('div');
                    indicator.className = 'speed-indicator';
                    indicator.style.pointerEvents = 'none';
                    playerArea.appendChild(indicator);
                }
                indicator.innerHTML = `当前加速 ${currentSpeedRate}x <span class="speed-arrow">▶▶</span>`;
                indicator.style.display = 'block';
            }
        }
    }

    function handleClick(e) {
        if (preventNextClick) {
            e.stopPropagation();
            preventNextClick = false;
        }
    }

    function hideSpeedIndicator() {
        const indicator = document.querySelector('.speed-indicator');
        if (indicator) {
            indicator.style.display = 'none';
        }
    }

    function addSpeedIndicatorStyle() {
        if (document.querySelector('.speed-indicator-style')) return;

        const style = document.createElement('style');
        style.className = 'speed-indicator-style';
        style.textContent = `
            .speed-indicator {
                position: absolute;
                top: 15%;
                left: 50%;
                transform: translateX(-50%);
                background: rgba(0, 0, 0, 0.7);
                color: white;
                padding: 5px 10px;
                border-radius: 4px;
                z-index: 999999;
                display: none;
                font-size: 14px;
                pointer-events: none;
            }
            .speed-arrow {
                color: #00a1d6;
                margin-left: 2px;
            }`;
        document.head.appendChild(style);
    }

    function initializeEvents() {
        addSpeedIndicatorStyle();

        document.addEventListener('mousedown', handleMouseDown, true);
        document.addEventListener('mouseup', handleMouseUp, true);
        document.addEventListener('click', handleClick, true);
        document.addEventListener('mouseleave', handleMouseUp, true);

        document.addEventListener('fullscreenchange', hideSpeedIndicator);
        document.addEventListener('webkitfullscreenchange', hideSpeedIndicator);
        document.addEventListener('mozfullscreenchange', hideSpeedIndicator);
        document.addEventListener('MSFullscreenChange', hideSpeedIndicator);

        document.addEventListener('pause', (e) => {
            if (e.target instanceof HTMLVideoElement) {
                hideSpeedIndicator();
            }
        }, true);

        if (window.location.hostname === 'www.bilibili.com') {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => {
                    setTimeout(initSubtitleAutoOpen, 1000);
                });
            } else {
                setTimeout(initSubtitleAutoOpen, 1000);
            }

            let lastUrl = location.href;
            urlObserver = new MutationObserver(() => {
                const url = location.href;
                if (url !== lastUrl) {
                    lastUrl = url;
                    setTimeout(() => {
                        checkAndInitVideoListener();
                    }, 500);
                }
            });
            urlObserver.observe(document, {subtree: true, childList: true});
        }
    }

    function cleanup() {
        if (animationFrameId) cancelAnimationFrame(animationFrameId);
        if (subtitleCheckTimer) clearTimeout(subtitleCheckTimer);
        if (urlObserver) urlObserver.disconnect();
    }

    window.addEventListener('unload', cleanup);

    // 菜单命令
    GM_registerMenuCommand('设置倍速值', () => {
        if (window.self !== window.top && window.location.hostname !== 'player.bilibili.com') return;
        const newSpeed = prompt('请输入新的倍速值(建议范围:1.1-4):', config.speedRate);
        if (newSpeed && !isNaN(newSpeed)) {
            config.speedRate = parseFloat(newSpeed);
            GM_setValue('speedRate', config.speedRate);
        }
    });

    GM_registerMenuCommand('重置字幕记忆', () => {
        saveGlobalSubtitleState(true);
    });

    initializeEvents();
})();