Greasy Fork

Greasy Fork is available in English.

视频控制增强

视频播放增强:1. 长按左键临时加速 2. B站自动开启AI字幕

当前为 2024-12-29 提交的版本,查看 最新版本

// ==UserScript==
// @name         视频控制增强
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  视频播放增强:1. 长按左键临时加速 2. B站自动开启AI字幕
// @author       Alonewinds
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';

    // ================ 通用配置和工具函数 ================
    const config = {
        speedRate: GM_getValue('speedRate', 2.0),
        minPressTime: 200,
        selectors: {
            'www.bilibili.com': '.bilibili-player-video-control-wrap, .squirtle-controller-wrap',
            'www.youtube.com': '.ytp-chrome-bottom, .ytp-progress-bar',
            'default': '.video-controls, .progress-bar, [role="slider"]'
        }
    };

    // ================ 倍速控制功能 ================
    let pressStartTime = 0;
    let originalSpeed = 1.0;
    let isPressed = false;
    let activeVideo = null;
    let isLongPress = false;
    let preventNextClick = false;

    // 注册倍速设置菜单
    GM_registerMenuCommand('设置倍速值', () => {
        const newSpeed = prompt('请输入新的倍速值(建议范围:1.1-4):', config.speedRate);
        if (newSpeed && !isNaN(newSpeed)) {
            config.speedRate = parseFloat(newSpeed);
            GM_setValue('speedRate', config.speedRate);
        }
    });

    function findVideoElement(element) {
        if (!element) return null;

        if (element instanceof HTMLVideoElement) {
            return element;
        }

        const domain = window.location.hostname;
        const controlSelector = config.selectors[domain] || config.selectors.default;

        if (element.closest(controlSelector)) {
            return null;
        }

        if (domain === 'www.youtube.com') {
            const ytPlayer = element.closest('.html5-video-player');
            return ytPlayer ? ytPlayer.querySelector('video') : 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 {
                    video.playbackRate = speed;
                    const moviePlayer = player.querySelector('.video-stream');
                    if (moviePlayer) {
                        moviePlayer.playbackRate = speed;
                    }
                    video.dispatchEvent(new Event('ratechange'));
                } catch (e) {
                    console.error('设置 YouTube 播放速度失败:', e);
                }
            }
        } else {
            video.playbackRate = speed;
        }
    }

    // ================ B站字幕功能 ================
    let subtitleEnabled = false;
    let subtitleAttempts = 0;
    let subtitleRetryTimer = null;
    let manuallyDisabled = false;
    let currentVideoId = '';
    let clickHandler = null;

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

    function enableSubtitle() {
        if (window.location.hostname !== 'www.bilibili.com') return;
        if (subtitleAttempts >= 5 || manuallyDisabled) return;

        subtitleAttempts++;

        const button = document.querySelector("div.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle > div.bpx-player-ctrl-btn-icon > span");
        if (!button) {
            subtitleRetryTimer = setTimeout(enableSubtitle, 1000);
            return;
        }

        const parent = button.closest('.bpx-player-ctrl-subtitle');
        if (!parent) return;

        if (clickHandler) {
            parent.removeEventListener('click', clickHandler);
        }

        clickHandler = (event) => {
            requestAnimationFrame(() => {
                if (!parent.classList.contains('bpx-player-ctrl-subtitle-on')) {
                    manuallyDisabled = true;
                    console.log('用户已手动关闭字幕,本视频将不再自动开启');
                }
            });
        };

        parent.addEventListener('click', clickHandler);

        if (!parent.classList.contains('bpx-player-ctrl-subtitle-on') && !manuallyDisabled) {
            button.click();
            subtitleEnabled = true;
        }
    }

    // ================ 事件处理 ================
    function handleMouseDown(e) {
        if (e.button !== 0) return;

        const video = findVideoElement(e.target);
        if (!video) return;

        pressStartTime = Date.now();
        activeVideo = video;
        originalSpeed = window.location.hostname === 'www.youtube.com'
            ? (video.closest('.html5-video-player').querySelector('.video-stream')?.playbackRate || video.playbackRate)
            : video.playbackRate;
        isPressed = true;
        isLongPress = false;
        preventNextClick = false;
    }

    function handleMouseUp(e) {
        if (!isPressed || !activeVideo) return;

        const pressDuration = Date.now() - pressStartTime;
        if (pressDuration >= config.minPressTime) {
            preventNextClick = true;
            setYouTubeSpeed(activeVideo, originalSpeed);
        }

        isPressed = false;
        isLongPress = false;
        activeVideo = null;
    }

    function handlePressDetection() {
        if (!isPressed || !activeVideo) return;

        const pressDuration = Date.now() - pressStartTime;
        if (pressDuration >= config.minPressTime) {
            if (activeVideo.playbackRate !== config.speedRate) {
                setYouTubeSpeed(activeVideo, config.speedRate);
            }
            isLongPress = true;
        }
    }

    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, 500);
            return;
        }

        function reset() {
            const newVideoId = getVideoId();
            if (newVideoId !== currentVideoId) {
                manuallyDisabled = false;
                currentVideoId = newVideoId;
                if (clickHandler) {
                    const oldButton = document.querySelector('.bpx-player-ctrl-subtitle');
                    if (oldButton) {
                        oldButton.removeEventListener('click', clickHandler);
                    }
                    clickHandler = null;
                }
            }

            subtitleEnabled = false;
            subtitleAttempts = 0;

            if (subtitleRetryTimer) {
                clearTimeout(subtitleRetryTimer);
                subtitleRetryTimer = null;
            }

            if (!manuallyDisabled) {
                setTimeout(enableSubtitle, 500);
            }
        }

        video.addEventListener('loadeddata', reset);
        video.addEventListener('play', () => {
            if (!subtitleEnabled && !manuallyDisabled) {
                reset();
            }
        });

        reset();
    }

    // ================ 初始化 ================
    function initializeEvents() {
        // 倍速控制事件
        document.addEventListener('mousedown', handleMouseDown, true);
        document.addEventListener('mouseup', handleMouseUp, true);
        document.addEventListener('click', handleClick, true);
        document.addEventListener('mouseleave', handleMouseUp, true);

        function checkPress() {
            handlePressDetection();
            requestAnimationFrame(checkPress);
        }
        checkPress();

        // B站字幕功能初始化
        if (window.location.hostname === 'www.bilibili.com') {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', onVideoLoad);
            } else {
                onVideoLoad();
            }
        }
    }

    initializeEvents();
})();