视频左上显示视频剩余时长

显示视频剩余时间和内置进度条

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

// ==UserScript==
// @name         视频左上显示视频剩余时长
// @author       He
// @version      1.3
// @description  显示视频剩余时间和内置进度条
// @match        *://*/*
// @exclude      *://*live*/*
// @exclude     *://www.huya.com/*
// @exclude     *://www.douyu.com/*
// @namespace https://greasyfork.org/users/808960
// ==/UserScript==

(function() {
    'use strict';

    // 创建显示容器的缓存,避免重复创建
    const containerCache = new WeakMap();

    /**
     * 设置视频时间显示组件
     * @param {HTMLVideoElement} video - 目标视频元素
     */
    function setupVideoTimeDisplay(video) {
        // 如果已经初始化过则跳过
        if (containerCache.has(video)) return;

        // 创建主容器
        const container = document.createElement('div');
        container.className = 'video-time-display-container';
        container.style.cssText = `
            position: absolute;
            left: 10px;
            top: 10px;
            z-index: 1000;
        `;

        // 时间显示容器 - 固定宽度确保数字居中
        const timeDisplay = document.createElement('div');
        timeDisplay.className = 'video-time-display';
        timeDisplay.style.cssText = `
            width: 100px; /* 固定宽度保证数字居中 */
            color: #C8DCC8;
            background: rgba(0, 0, 0, 0.5);
            padding: 3px 1px 8px 1px; /* 下边距预留进度条空间 */
            font-size: 15px;
            text-align: center;
            border-radius: 5px;
            position: relative; /* 用于子元素绝对定位 */
        `;

        // 剩余时间显示元素
        const timeText = document.createElement('div');
        timeText.className = 'video-time-text';
        timeText.style.cssText = `
            line-height: 1.2;
            user-select: none;
        `;

        // 进度条容器 (整合到时间容器内部)
        const progressBar = document.createElement('div');
        progressBar.className = 'video-progress-bar';
        progressBar.style.cssText = `
            width: 100%;
            height: 2px;
            background: rgba(255, 255, 255, 0.3);
            position: absolute;
            bottom: 3px;
            left: 0;
            overflow: hidden;
        `;

        // 缓冲进度条
        const bufferedBar = document.createElement('div');
        bufferedBar.className = 'video-buffered-bar';
        bufferedBar.style.cssText = `
            width: 0%;
            height: 100%;
            background: #FF6A00;
            position: absolute;
            left: 0;
            transition: width 0.3s ease; /* 平滑过渡效果 */
        `;

        // 播放进度条
        const progressBarInner = document.createElement('div');
        progressBarInner.className = 'video-progress-bar-inner';
        progressBarInner.style.cssText = `
            width: 0%;
            height: 100%;
            background: skyblue;
            position: absolute;
            left: 0;
            transition: width 0.3s ease;
        `;

        // 组装DOM结构
        progressBar.append(bufferedBar, progressBarInner);
        timeDisplay.append(timeText, progressBar);
        container.append(timeDisplay);

        // 寻找最近的relative定位父容器
        let parent = video.parentElement;
        while (parent && getComputedStyle(parent).position !== 'relative') {
            parent = parent.parentElement;
        }
        (parent || document.body).append(container);

        // 缓存容器引用
        containerCache.set(video, container);

        // 优化:使用requestAnimationFrame进行更新
        let isUpdating = false;

        /**
         * 更新时间和进度条显示
         */
        const updateDisplay = () => {
            if (isUpdating) return;
            isUpdating = true;

            requestAnimationFrame(() => {
                // 确保视频时长有效
                if (!isFinite(video.duration)) {
                    isUpdating = false;
                    return;
                }

                // 计算剩余时间
                const remaining = video.duration - video.currentTime;
                const mins = String(Math.floor(remaining / 60)).padStart(2, '0');
                const secs = String(Math.floor(remaining % 60)).padStart(2, '0');
                timeText.textContent = `${mins}:${secs}`;

                // 更新播放进度
                const progressPercent = (video.currentTime / video.duration) * 100;
                progressBarInner.style.width = `${progressPercent}%`;

                // 更新缓冲进度 (优化算法)
                if (video.buffered.length > 0) {
                    // 获取最后一个时间区间
                    const lastBuffer = video.buffered.end(video.buffered.length - 1);
                    const bufferPercent = (lastBuffer / video.duration) * 100;
                    bufferedBar.style.width = `${bufferPercent}%`;
                }

                isUpdating = false;
            });
        };

        // 绑定事件监听 (使用被动事件优化滚动性能)
        const events = ['timeupdate', 'progress', 'loadedmetadata'];
        events.forEach(e => video.addEventListener(e, updateDisplay, { passive: true }));

        // 初始显示
        updateDisplay();
    }

    /* DOM观察器配置 */
    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            // 仅处理新增节点
            for (const node of mutation.addedNodes) {
                // 深度扫描video元素
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'VIDEO') {
                        setupVideoTimeDisplay(node);
                    }
                    // 扫描子节点中的video元素
                    else if (node.querySelector('video')) {
                        node.querySelectorAll('video').forEach(setupVideoTimeDisplay);
                    }
                }
            }
        }
    });

    // 启动观察 (优化:仅观察子节点变化)
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });

    // 初始化已存在的视频
    document.querySelectorAll('video').forEach(setupVideoTimeDisplay);
})();