Greasy Fork is available in English.

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

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

// ==UserScript==
// @name         视频临时倍速+B站字幕开关记忆
// @namespace    http://tampermonkey.net/
// @version      2.4
// @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
// ==/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',  // 添加 YouTube 选择器
            'default': '.video-controls, .progress-bar, [role="slider"]'
        },
        // 调试模式开关
        debug: false
    };

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

    // B站字幕相关变量
    let currentVideoId = '';
    let want_open = false;
    let subtitleCheckTimer = null;
    let debounceTimer = null;
    let lastSubtitleState = null;
    let lastSubtitleCheckTime = 0;

    // 添加动画帧ID跟踪
    let animationFrameId = null;
    let urlObserver = null;

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

    // 添加开始检测函数
    function startPressDetection() {
        if (!animationFrameId) {
            function checkPress() {
                handlePressDetection();
                animationFrameId = requestAnimationFrame(checkPress);
            }
            checkPress();
        }
    }

    // 添加停止检测函数
    function stopPressDetection() {
        if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
            animationFrameId = null;
        }
    }

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

            const indicator = document.querySelector('.speed-indicator');
            if (indicator) {
                indicator.innerHTML = `当前加速 ${config.speedRate}x <span class="speed-arrow">▶▶</span>`;
            }
        }
    });

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

        if (element instanceof HTMLVideoElement) {
            return element;
        }

        const domain = window.location.hostname;

        // B站和YouTube使用区域限制
        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) {  // 只在加速时监控
                        // 增加检查间隔到 100ms
                        player._speedInterval = setInterval(() => {
                            if (video.playbackRate !== speed) {
                                video.playbackRate = speed;
                            }
                        }, 100);

                        // 添加超时清理
                        setTimeout(() => {
                            if (player._speedInterval) {
                                clearInterval(player._speedInterval);
                                player._speedInterval = null;
                            }
                        }, 5000);  // 5秒后自动清理
                    }

                    video.dispatchEvent(new Event('ratechange'));
                } catch (e) {
                    console.error('设置 YouTube 播放速度失败:', e);
                }
            }
        } else {
            video.playbackRate = speed;
        }
    }

    // ================ B站字幕功能 ================
    function getVideoId() {
        const match = location.pathname.match(/\/video\/(.*?)\//);
        return match ? match[1] : '';
    }


    const browserMode = (function() {
        const mode_data = navigator.userAgent;
        if (mode_data.includes('Firefox')) {
            return 'Firefox';
        } else if (mode_data.includes('Chrome')) {
            return 'Chrome';
        } else return 'Chrome';
    })();


    function isAiSubtitle() {
        let sub = document.querySelector('.bpx-player-ctrl-subtitle-major-inner > .bpx-state-active');
        if (sub && sub.innerText.includes("自动")) return true;
        return false;
    }


    function isSubtitleOpen() {
        const now = Date.now();

        if (lastSubtitleState !== null && now - lastSubtitleCheckTime < 500) {
            return lastSubtitleState;
        }

        let max_length = 3;
        if (browserMode === 'Firefox') max_length = 2;
        let sub = document.querySelectorAll('svg[preserveAspectRatio="xMidYMid meet"] > defs > filter');

        lastSubtitleCheckTime = now;
        lastSubtitleState = (sub.length !== max_length);
        return lastSubtitleState;
    }


    function isRememberOpen() {
        return GM_getValue('subtitleOpen', false);
    }

    // 开启字幕 - 添加防重复执行
    function openSubtitle() {
        // 清除之前的定时器
        if (subtitleCheckTimer) {
            clearTimeout(subtitleCheckTimer);
            subtitleCheckTimer = null;
        }

        let sub = document.querySelector('[aria-label="字幕"] [class="bpx-common-svg-icon"]');
        if (!sub) {
            sub = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle .bpx-player-ctrl-btn-icon');
        }

        if (!sub) {
            // 如果没找到字幕按钮,延迟重试
            subtitleCheckTimer = setTimeout(openSubtitle, 2000);
            return;
        }

        debugLog('尝试开启字幕', isRememberOpen());

        const currentState = isSubtitleOpen();
        const desiredState = isRememberOpen();


        if (currentState !== desiredState && !want_open) {
            want_open = true;
            setTimeout(() => {
                if (sub) sub.click();
                want_open = false;
                debugLog('已点击字幕按钮');
            }, 300);
        }

        rememberSwitch();
    }

    // 记忆开关状态回调函数 - 添加防抖
    function rememberSwitchCallback(e) {
        if (!e.isTrusted) return;

        if (debounceTimer) {
            clearTimeout(debounceTimer);
        }

        debounceTimer = setTimeout(() => {
            const isOpen = isSubtitleOpen();
            GM_setValue('subtitleOpen', isOpen);
            debugLog('储存字幕开关状态', isOpen);
            debounceTimer = null;
        }, 300);
    }

    // 记忆开关状态 - 优化事件监听
    function rememberSwitch() {
        let sub = document.querySelector('div[aria-label="字幕"]');
        if (!sub) {
            sub = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle');
        }

        if (sub && !sub._hasSubtitleListener) {
            // 使用属性标记已添加监听器,避免重复添加
            sub._hasSubtitleListener = true;
            sub.addEventListener('click', rememberSwitchCallback);
            debugLog('已添加字幕按钮点击监听');
        }
    }

    function handleMouseDown(e) {
        if (e.button !== 0) return;

        const domain = window.location.hostname;
        let video = null;
        let playerArea = null;

        // B站和YouTube使用严格区域限制
        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 (!video) return;

        if (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';
                    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;
            return;
        }
    }

    // 优化视频加载处理 - 合并事件监听
    function onVideoLoad() {
        if (window.location.hostname !== 'www.bilibili.com') return;

        const video = document.querySelector('video');
        if (!video) {
            setTimeout(onVideoLoad, 1000); // 增加延迟,减少检查频率
            return;
        }

        const newVideoId = getVideoId();
        if (newVideoId !== currentVideoId) {
            currentVideoId = newVideoId;
            // 重置字幕状态缓存
            lastSubtitleState = null;
            lastSubtitleCheckTime = 0;

            // 视频ID变化时,初始化字幕功能
            setTimeout(openSubtitle, 1500);
        }


        if (!video._hasEvents) {
            video._hasEvents = true;


            const handleVideoEvent = () => {
                // 清除之前的定时器
                if (subtitleCheckTimer) {
                    clearTimeout(subtitleCheckTimer);
                }
                // 延迟检查字幕,避免频繁调用
                subtitleCheckTimer = setTimeout(openSubtitle, 1500);
            };

            // 视频加载和播放时,检查字幕状态
            video.addEventListener('loadeddata', handleVideoEvent);
            video.addEventListener('play', handleVideoEvent);
        }
    }

    // ================ 初始化 ================
    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', onVideoLoad);
            } else {
                onVideoLoad();
            }

            // 优化URL变化监听 - 添加节流
            let lastUrl = location.href;
            let urlChangeTimer = null;

            // 清理之前的观察器
            if (urlObserver) {
                urlObserver.disconnect();
                urlObserver = null;
            }

            urlObserver = new MutationObserver(() => {
                const url = location.href;
                if (url !== lastUrl) {
                    // 清除之前的定时器
                    if (urlChangeTimer) {
                        clearTimeout(urlChangeTimer);
                    }

                    // 延迟处理URL变化,避免频繁触发
                    urlChangeTimer = setTimeout(() => {
                        lastUrl = url;
                        onVideoLoad();
                        urlChangeTimer = null;
                    }, 500);
                }
            });

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

            // 初始化字幕功能 - 优化检测逻辑
            let initAttempts = 0;
            let initTimer = setInterval(() => {
                let k = document.querySelector('div[aria-label="宽屏"]');
                initAttempts++;

                if (k || initAttempts > 20) {
                    clearInterval(initTimer);
                    if (k) openSubtitle();
                }
            }, 200); // 增加间隔,减少检查频率
        }
    }

    function addSpeedIndicatorStyle() {
        const style = document.createElement('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;
            }
            .speed-arrow {
                color: #00a1d6;
                margin-left: 2px;
            }`;
        document.head.appendChild(style);
    }

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

    // 清理函数 - 在页面卸载时清理资源
    function cleanup() {
        if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
        }

        if (subtitleCheckTimer) {
            clearTimeout(subtitleCheckTimer);
        }

        if (debounceTimer) {
            clearTimeout(debounceTimer);
        }

        if (urlObserver) {
            urlObserver.disconnect();
        }
    }

    // 注册页面卸载事件
    window.addEventListener('unload', cleanup);

    initializeEvents();
})();