Greasy Fork

Greasy Fork is available in English.

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

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

当前为 2025-03-12 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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