Greasy Fork is available in English.
显示视频剩余时间和内置进度条
当前为
// ==UserScript==
// @name 视频左上显示视频剩余时长
// @author He
// @version 1.3
// @description 显示视频剩余时间和内置进度条
// @match *://*/*
// @exclude *://*live*/*
// @exclude *://www.huya.com/*
// @exclude *://www.douyu.com/*
// @namespace http://greasyfork.icu/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);
})();