// ==UserScript== // @name 智能全视频进度条 // @namespace http://tampermonkey.net/ // @version 20.0 // @description 所有视频下方增加进度条 // @author Tencent Yuanbao & KM // @match *://*/* // @grant none // @run-at document-start // ==/UserScript== (() => { 'use strict'; const MAX_RETRY_COUNT = 7; const RETRY_BASE_DELAY = 100; const AHEAD_SEC = 0.050; // 提前50ms let isInitialized = false; let currentVideo = null; let progressBar = null; let progressInner = null; let videoObserver = null; let urlObserver = null; let retryCount = 0; let rafId = 0; console.log('[RVP-50ms] 提前50ms领跑版启动'); /* ======== ShadowDOM劫持 ======== */ const hijackShadow = () => { const orig = Element.prototype.attachShadow; Element.prototype.attachShadow = function (opts) { const root = orig.call(this, opts); new MutationObserver(() => { if (!isInitialized) immediateVideoDetection(); }) .observe(root, { childList: true, subtree: true }); return root; }; }; try { hijackShadow(); } catch (_) {} /* ======== 入口 ======== */ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', robustInit); setTimeout(robustInit, 100); } else { robustInit(); } function robustInit() { if (isInitialized) return; prepareProgressBarSlot(); // 1. 先占坑 immediateVideoDetection(); // 2. 再检测 setupRobustObserver(); setupURLObserver(); startPeriodicCheck(); watchLateVideo(); document.addEventListener('visibilitychange', handleVisibilityChange); } /* ======== 1. 占位进度条 ======== */ function prepareProgressBarSlot() { if (document.querySelector('#robust-video-progress-bar')) return; const slot = document.createElement('div'); slot.id = 'robust-video-progress-bar'; Object.assign(slot.style, { position: 'absolute', bottom: '0', left: '0', width: '100%', height: '3px', background: 'rgba(0,0,0,0)', zIndex: '2147483647', pointerEvents: 'none', transition: 'height .2s', willChange: 'transform' }); const inner = document.createElement('div'); inner.id = 'robust-progress-inner'; Object.assign(inner.style, { height: '100%', width: '0%', background: 'linear-gradient(90deg,#ff0000,#ff3333)', willChange: 'width' }); slot.appendChild(inner); document.body.appendChild(slot); progressBar = slot; progressInner = inner; } /* ======== 2. 视频检测 ======== */ function immediateVideoDetection() { if (isInitialized) return; retryCount++; const videos = findVideoElementsRobust(); if (videos.length) { initProgressBarRobust(selectBestVideo(videos)); return; } if (retryCount < MAX_RETRY_COUNT) { const delay = RETRY_BASE_DELAY * Math.pow(2, retryCount - 1); setTimeout(immediateVideoDetection, delay); } } /* ======== 3. 查找video(含Shadow) ======== */ function findVideoElementsRobust() { let videos = Array.from(document.querySelectorAll('video')); document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) videos.push(...el.shadowRoot.querySelectorAll('video')); }); return videos.filter(v => { try { const r = v.getBoundingClientRect(), s = getComputedStyle(v); return r.width > 10 && r.height > 10 && s.visibility !== 'hidden' && s.display !== 'none' && s.opacity !== '0'; } catch (_) { return false; } }); } /* ======== 4. 选最佳 ======== */ function selectBestVideo(videos) { return videos.reduce((best, v) => { const area = v.getBoundingClientRect().width * v.getBoundingClientRect().height; let score = area; if (!v.paused) score += 1e5; if (v.readyState >= 3) score += 5e4; if (v.duration > 0) score += 3e4; return !best || score > best.score ? { video: v, score } : best; }, null)?.video; } /* ======== 5. 初始化进度条 ======== */ function initProgressBarRobust(video) { if (isInitialized || !video) return false; try { const container = findBestContainerRobust(video); if (!container) return false; ensureContainerPositioning(container); if (progressBar.parentElement !== container) container.appendChild(progressBar); Object.assign(progressBar.style, { position: 'absolute', bottom: '0', left: '0', width: '100%', pointerEvents: 'auto' }); currentVideo = video; bindVideoEventsRobust(); startRAFUpdate(); isInitialized = true; retryCount = 0; return true; } catch (_) { return false; } } /* ======== 6. RAF提前50ms更新 ======== */ function startRAFUpdate() { cancelRAFUpdate(); const loop = () => { if (!currentVideo) return; const dur = currentVideo.duration, cur = currentVideo.currentTime; if (dur > 0 && progressInner) { const ahead = cur + AHEAD_SEC; progressInner.style.width = `${Math.min(100, (ahead / dur) * 100)}%`; } rafId = requestAnimationFrame(loop); }; rafId = requestAnimationFrame(loop); } function cancelRAFUpdate() { cancelAnimationFrame(rafId); } /* ======== 7. 绑定事件 + 热插拔 + 纠偏 ======== */ function bindVideoEventsRobust() { if (!currentVideo) return false; currentVideo.addEventListener('play', startRAFUpdate, { passive: true }); currentVideo.addEventListener('pause', cancelRAFUpdate, { passive: true }); currentVideo.addEventListener('ended', cancelRAFUpdate, { passive: true }); currentVideo.addEventListener('emptied', handleVideoReplaced, { once: true }); currentVideo.addEventListener('loadstart', handleVideoReplaced, { once: true }); currentVideo.addEventListener('timeupdate', () => { if (progressInner) progressInner.style.width = `${(currentVideo.currentTime / currentVideo.duration) * 100}%`; }, { passive: true }); startRAFUpdate(); return true; } /* ======== 8. 热插拔回调 ======== */ function handleVideoReplaced() { cancelRAFUpdate(); setTimeout(() => { isInitialized = false; currentVideo = null; retryCount = 0; immediateVideoDetection(); }, 100); } /* ======== 9. 保护机制 ======== */ function setupProgressBarProtection(bar, container) { const id = setInterval(() => { if (!container.isConnected || !document.contains(bar)) { clearInterval(id); cancelRAFUpdate(); isInitialized = false; currentVideo = null; progressBar = null; progressInner = null; setTimeout(immediateVideoDetection, 1000); } }, 3000); setTimeout(() => clearInterval(id), 600000); } /* ======== 10. 靶向observer ======== */ function setupRobustObserver() { if (videoObserver) return; videoObserver = new MutationObserver(muts => { for (const m of muts) for (const n of m.addedNodes) { if (n.nodeType === 1 && (n.tagName === 'VIDEO' || n.querySelector?.('video'))) { if (!isInitialized) immediateVideoDetection(); return; } } }); videoObserver.observe(document.body, { childList: true, subtree: false }); } /* ======== 11. URL变化 ======== */ function setupURLObserver() { if (urlObserver) return; let last = location.href; urlObserver = new MutationObserver(() => { if (location.href !== last) { last = location.href; cancelRAFUpdate(); isInitialized = false; currentVideo = null; retryCount = 0; setTimeout(immediateVideoDetection, 500); } }); urlObserver.observe(document, { subtree: true, childList: true }); } /* ======== 12. 后期容器监听 ======== */ function watchLateVideo() { [ '[data-testid="video"]', '[class*="video-wrapper"]', '[class*="player-container"]', '#video-player', 'iframe[src*="youtube"]', 'iframe[src*="bilibili"]' ].forEach(sel => { const el = document.querySelector(sel); if (el) new MutationObserver(() => { if (!isInitialized) immediateVideoDetection(); }).observe(el, { childList: true, subtree: true }); }); } /* ======== 13. 周期兜底 ======== */ function startPeriodicCheck() { const id = setInterval(() => { if (!isInitialized && retryCount < MAX_RETRY_COUNT) immediateVideoDetection(); else if (isInitialized) { if (!progressBar || !document.contains(progressBar)) { cancelRAFUpdate(); isInitialized = false; immediateVideoDetection(); } } else clearInterval(id); }, 2000); } /* ======== 14. 可见性 ======== */ function handleVisibilityChange() { if (!document.hidden && !isInitialized) setTimeout(immediateVideoDetection, 300); } /* ======== 15. 工具 ======== */ function findBestContainerRobust(video) { if (!video) return null; let c = video.parentElement, best = c; for (let i = 0; i < 4 && c && c !== document.body; i++, c = c.parentElement) { try { const vr = video.getBoundingClientRect(), cr = c.getBoundingClientRect(); if (Math.abs(cr.width - vr.width) < 150 && Math.abs(cr.height - vr.height) < 150) { best = c; break; } } catch (_) {} } return best; } function ensureContainerPositioning(c) { try { if (getComputedStyle(c).position === 'static') c.style.position = 'relative'; } catch (_) {} } })();