Greasy Fork

来自缓存

Greasy Fork is available in English.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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