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