Greasy Fork

Greasy Fork is available in English.

智能视频进度条

自动为页面视频添加红色进度条

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         智能视频进度条
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  自动为页面视频添加红色进度条
// @author       Optimized by Qwen
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // 配置常量
    const CONFIG = {
        MAX_RETRY_COUNT: 7,
        RETRY_BASE_DELAY: 100,
        AHEAD_MS: 50, // 提前50ms
        PROGRESS_HEIGHT: '3px',
        PROGRESS_COLOR: 'linear-gradient(90deg, #ff0000, #ff3333)',
        Z_INDEX: 2147483647
    };

    // 全局状态
    const state = {
        isInitialized: false,
        currentVideo: null,
        progressBar: null,
        progressInner: null,
        videoObserver: null,
        urlObserver: null,
        retryCount: 0,
        rafId: 0,
        lastUrl: location.href
    };

    console.log('[Optimized Video Progress Bar] 启动');

    // 初始化入口
    function init() {
        if (state.isInitialized) return;
        
        createProgressBar();
        setupVideoDetection();
        setupUrlObserver();
        setupPageVisibilityListener();
        
        // 立即检测视频
        detectVideo();
    }

    // 创建进度条DOM元素
    function createProgressBar() {
        if (document.querySelector('#optimized-video-progress')) return;
        
        const progressBar = document.createElement('div');
        progressBar.id = 'optimized-video-progress';
        progressBar.innerHTML = '<div id="optimized-progress-inner"></div>';
        
        // 设置样式
        Object.assign(progressBar.style, {
            position: 'absolute',
            bottom: '0',
            left: '0',
            width: '100%',
            height: CONFIG.PROGRESS_HEIGHT,
            background: 'transparent',
            zIndex: CONFIG.Z_INDEX,
            pointerEvents: 'none',
            transition: 'height 0.2s',
            willChange: 'transform'
        });
        
        const progressInner = progressBar.querySelector('#optimized-progress-inner');
        Object.assign(progressInner.style, {
            height: '100%',
            width: '0%',
            background: CONFIG.PROGRESS_COLOR,
            willChange: 'width'
        });
        
        document.body.appendChild(progressBar);
        
        state.progressBar = progressBar;
        state.progressInner = progressInner;
    }

    // 设置视频检测机制
    function setupVideoDetection() {
        // 监听DOM变化
        state.videoObserver = new MutationObserver((mutations) => {
            let shouldDetect = false;
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) {
                        if (node.tagName === 'VIDEO' || node.querySelector?.('video')) {
                            shouldDetect = true;
                        }
                    }
                });
            });
            
            if (shouldDetect && !state.isInitialized) {
                setTimeout(detectVideo, 100);
            }
        });
        
        state.videoObserver.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        // 监听Shadow DOM
        hijackShadowDOM();
    }

    // 劫持Shadow DOM以检测其中的视频元素
    function hijackShadowDOM() {
        const originalAttachShadow = Element.prototype.attachShadow;
        if (originalAttachShadow) {
            Element.prototype.attachShadow = function(options) {
                const shadowRoot = originalAttachShadow.call(this, options);
                const shadowObserver = new MutationObserver(() => {
                    if (!state.isInitialized) {
                        setTimeout(detectVideo, 100);
                    }
                });
                shadowObserver.observe(shadowRoot, {
                    childList: true,
                    subtree: true
                });
                return shadowRoot;
            };
        }
    }

    // 检测页面中的视频元素
    function detectVideo() {
        if (state.isInitialized) return;
        
        state.retryCount++;
        const videos = findVideos();
        
        if (videos.length > 0) {
            const bestVideo = selectBestVideo(videos);
            if (bestVideo && initVideoHandler(bestVideo)) {
                return;
            }
        }
        
        // 如果没有找到视频,设置重试机制
        if (state.retryCount < CONFIG.MAX_RETRY_COUNT) {
            const delay = CONFIG.RETRY_BASE_DELAY * Math.pow(2, state.retryCount - 1);
            setTimeout(detectVideo, delay);
        }
    }

    // 查找所有视频元素(包括Shadow DOM中的)
    function findVideos() {
        let videos = Array.from(document.querySelectorAll('video'));
        
        // 查找Shadow DOM中的视频
        const allElements = document.querySelectorAll('*');
        allElements.forEach(element => {
            if (element.shadowRoot) {
                videos = videos.concat(Array.from(element.shadowRoot.querySelectorAll('video')));
            }
        });
        
        // 过滤出可见的视频
        return videos.filter(video => {
            try {
                const rect = video.getBoundingClientRect();
                const style = getComputedStyle(video);
                
                return rect.width > 10 && 
                       rect.height > 10 && 
                       style.visibility !== 'hidden' &&
                       style.display !== 'none' &&
                       style.opacity !== '0' &&
                       video.src || video.children.length > 0;
            } catch (e) {
                return false;
            }
        });
    }

    // 选择最佳视频(基于播放状态、尺寸、就绪状态等)
    function selectBestVideo(videos) {
        return videos.reduce((best, video) => {
            const rect = video.getBoundingClientRect();
            const area = rect.width * rect.height;
            
            // 计算分数:播放状态、就绪状态、持续时间、尺寸
            let score = area;
            if (!video.paused) score += 100000;  // 正在播放的视频优先
            if (video.readyState >= 3) score += 50000;  // 就绪状态好的优先
            if (video.duration > 0) score += 30000;  // 有持续时间的优先
            
            if (!best || score > best.score) {
                return { video, score };
            }
            return best;
        }, null)?.video;
    }

    // 初始化视频处理
    function initVideoHandler(video) {
        if (state.isInitialized || !video) return false;
        
        try {
            const container = findBestContainer(video);
            if (!container) return false;
            
            setupContainer(container);
            if (state.progressBar.parentElement !== container) {
                container.appendChild(state.progressBar);
            }
            
            state.currentVideo = video;
            bindVideoEvents();
            startRAFLoop();
            
            state.isInitialized = true;
            state.retryCount = 0;
            
            console.log('[Optimized Video Progress Bar] 进度条已绑定到视频');
            return true;
        } catch (error) {
            console.error('[Optimized Video Progress Bar] 初始化失败:', error);
            return false;
        }
    }

    // 查找最佳容器
    function findBestContainer(video) {
        if (!video) return null;
        
        let container = video.parentElement;
        let bestContainer = container;
        
        // 向上查找4层,寻找与视频尺寸相近的容器
        for (let i = 0; i < 4 && container && container !== document.body; i++) {
            try {
                const videoRect = video.getBoundingClientRect();
                const containerRect = container.getBoundingClientRect();
                
                const widthDiff = Math.abs(containerRect.width - videoRect.width);
                const heightDiff = Math.abs(containerRect.height - videoRect.height);
                
                if (widthDiff < 150 && heightDiff < 150) {
                    bestContainer = container;
                    break;
                }
                
                container = container.parentElement;
            } catch (e) {
                break;
            }
        }
        
        return bestContainer;
    }

    // 设置容器样式
    function setupContainer(container) {
        try {
            if (getComputedStyle(container).position === 'static') {
                container.style.position = 'relative';
            }
        } catch (e) {
            console.warn('[Optimized Video Progress Bar] 设置容器样式失败:', e);
        }
    }

    // 绑定视频事件
    function bindVideoEvents() {
        if (!state.currentVideo) return;
        
        const video = state.currentVideo;
        
        // 播放事件
        video.addEventListener('play', startRAFLoop, { passive: true });
        video.addEventListener('pause', stopRAFLoop, { passive: true });
        video.addEventListener('ended', stopRAFLoop, { passive: true });
        
        // 视频源变化事件
        video.addEventListener('loadstart', handleVideoChange, { once: true });
        video.addEventListener('emptied', handleVideoChange, { once: true });
        
        // 时间更新事件(作为RAF的补充)
        video.addEventListener('timeupdate', updateProgressBar, { passive: true });
    }

    // 处理视频变化(如重新加载)
    function handleVideoChange() {
        stopRAFLoop();
        setTimeout(() => {
            state.isInitialized = false;
            state.currentVideo = null;
            state.retryCount = 0;
            detectVideo();
        }, 100);
    }

    // RAF循环更新进度条
    function startRAFLoop() {
        stopRAFLoop(); // 先停止之前的循环
        
        const update = () => {
            if (!state.currentVideo) return;
            
            updateProgressBar();
            state.rafId = requestAnimationFrame(update);
        };
        
        state.rafId = requestAnimationFrame(update);
    }

    // 停止RAF循环
    function stopRAFLoop() {
        if (state.rafId) {
            cancelAnimationFrame(state.rafId);
            state.rafId = 0;
        }
    }

    // 更新进度条显示
    function updateProgressBar() {
        if (!state.currentVideo || !state.progressInner) return;
        
        const { duration, currentTime } = state.currentVideo;
        
        if (duration > 0) {
            // 计算提前时间后的进度
            const aheadTime = currentTime + (CONFIG.AHEAD_MS / 1000);
            const progress = Math.min(1, Math.max(0, aheadTime / duration));
            const percentage = progress * 100;
            
            state.progressInner.style.width = `${percentage}%`;
        }
    }

    // 设置URL变化监听
    function setupUrlObserver() {
        // 监听URL变化
        const urlObserver = new MutationObserver(() => {
            if (location.href !== state.lastUrl) {
                state.lastUrl = location.href;
                resetState();
                setTimeout(detectVideo, 500);
            }
        });
        
        urlObserver.observe(document, { subtree: true, childList: true });
        state.urlObserver = urlObserver;
    }

    // 设置页面可见性监听
    function setupPageVisibilityListener() {
        document.addEventListener('visibilitychange', () => {
            if (!document.hidden && !state.isInitialized) {
                setTimeout(detectVideo, 300);
            }
        });
    }

    // 重置状态
    function resetState() {
        stopRAFLoop();
        state.isInitialized = false;
        state.currentVideo = null;
        state.retryCount = 0;
    }

    // 根据页面加载状态初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
        setTimeout(init, 100);
    } else {
        init();
    }

    // 页面卸载时清理
    window.addEventListener('beforeunload', () => {
        stopRAFLoop();
        if (state.videoObserver) {
            state.videoObserver.disconnect();
        }
        if (state.urlObserver) {
            state.urlObserver.disconnect();
        }
    });
})();