// ==UserScript== // @name 全网页强制倍速 // @namespace https://workbuddy.cn // @version 1.1.0 // @description 在所有网页强制锁定视频倍速(电脑/手机通用):原生接口劫持+高频守护+动态视频+iframe 支持;面板可折叠缩小 // @match *://*/* // @grant none // @run-at document-idle // @author WorkBuddy // ==/UserScript== (function(){ function initForceSpeed(){ if (window.__forceSpeedReady) return; window.__forceSpeedReady = true; var currentRate = 1; var forceAutoRate = false; var NATIVE_RATE_GET = null, NATIVE_RATE_SET = null; try { var nd = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); if (nd && nd.get && nd.set) { NATIVE_RATE_GET = nd.get; NATIVE_RATE_SET = nd.set; } } catch (e) {} function getAllVideos(){ var vids = []; (function collect(doc, depth){ if (depth > 6) return; try { var vs = doc.querySelectorAll('video'); for (var i = 0; i < vs.length; i++) vids.push(vs[i]); var fs = doc.querySelectorAll('iframe'); for (var j = 0; j < fs.length; j++) { try { if (fs[j].contentDocument) collect(fs[j].contentDocument, depth + 1); } catch (e) {} } } catch (e) {} })(document, 0); return vids; } function forceVideoRate(v, rate){ try { if (NATIVE_RATE_SET) { try { NATIVE_RATE_SET.call(v, rate); } catch (e) {} } else { v.playbackRate = rate; } try { if (v.defaultPlaybackRate !== rate) v.defaultPlaybackRate = rate; } catch (e) {} if (v.__lockRate !== rate) { v.__lockRate = rate; try { Object.defineProperty(v, 'playbackRate', { configurable: true, enumerable: true, get: function(){ try { return NATIVE_RATE_GET ? NATIVE_RATE_GET.call(v) : rate; } catch (e) { return rate; } }, set: function(){} }); } catch (e) {} } if (Math.abs(v.playbackRate - rate) > 0.01 && NATIVE_RATE_SET) { try { NATIVE_RATE_SET.call(v, rate); } catch (e) {} } } catch (e) {} } function applyRateLocal(rate){ try { var vs = document.querySelectorAll('video'); for (var i = 0; i < vs.length; i++) forceVideoRate(vs[i], rate); } catch (e) {} } function broadcastRate(rate){ (function send(win){ try { win.postMessage({ __forceSpeedRate: rate }, '*'); } catch (e) {} var frames, len = 0; try { frames = win.frames; len = frames.length; } catch (e) { return; } for (var i = 0; i < len; i++) { try { send(frames[i]); } catch (e) {} } })(window); } function unlockVideos(){ var vs = getAllVideos(); for (var i = 0; i < vs.length; i++) { try { delete vs[i].playbackRate; } catch (e) {} try { delete vs[i].__lockRate; } catch (e) {} } } function setRate(rate){ currentRate = rate; applyRateLocal(rate); var vs = getAllVideos(); for (var i = 0; i < vs.length; i++) forceVideoRate(vs[i], rate); broadcastRate(rate); } var frameRate = 0; try { window.addEventListener('message', function(e){ if (e.source === window) return; var d = e.data; if (!d || typeof d !== 'object' || typeof d.__forceSpeedRate !== 'number') return; if (d.__forceSpeedRate < 0) { frameRate = 0; unlockVideos(); applyRateLocal(0); return; } frameRate = d.__forceSpeedRate; applyRateLocal(d.__forceSpeedRate); }); } catch (e) {} try { document.addEventListener('ratechange', function(e){ var v = e.target; if (!v || v.tagName !== 'VIDEO') return; var target = forceAutoRate ? currentRate : frameRate; if (!target) return; if (Math.abs(v.playbackRate - target) > 0.01) { setTimeout(function(){ forceVideoRate(v, target); }, 50); } }, true); } catch (e) {} var rateTimer = null; function startRateKeeper(){ if (rateTimer) return; rateTimer = setInterval(function(){ if (!forceAutoRate) return; applyRateLocal(currentRate); var vs = getAllVideos(); for (var i = 0; i < vs.length; i++) forceVideoRate(vs[i], currentRate); broadcastRate(currentRate); }, 500); } function stopRateKeeper(){ if (rateTimer) { clearInterval(rateTimer); rateTimer = null; } } try { var mo = new MutationObserver(function(){ if (!forceAutoRate) return; applyRateLocal(currentRate); var vs = getAllVideos(); for (var i = 0; i < vs.length; i++) forceVideoRate(vs[i], currentRate); }); mo.observe(document, { childList: true, subtree: true }); } catch (e) {} var box, head, badge, minBtn; function updateBadge(){ if (badge) badge.textContent = forceAutoRate ? (currentRate + 'x') : '⚡'; } function setSpeed(r){ forceAutoRate = true; currentRate = r; setRate(r); startRateKeeper(); updateBadge(); } var speeds = [0.5, 1, 1.25, 1.5, 1.75, 2, 3, 4, 8, 16]; if (document.getElementById('force-speed-root')) return; var root = document.createElement('div'); root.id = 'force-speed-root'; root.style.cssText = 'position:fixed;top:80px;right:12px;z-index:2147483647;'; document.documentElement.appendChild(root); var shadow = root.attachShadow({ mode: 'open' }); shadow.innerHTML = '' + '
' + '' + '
' + '
点选倍速立即锁定(平台无法改回)
' + '
' + '
关闭强制倍速
' + '
拖动标题栏移动;点「-」缩小成一条,点 ✕ 隐藏为右下角小圆标。
' + '
' + '
⚡
'; var row = shadow.getElementById('row'); box = shadow.getElementById('box'); head = shadow.getElementById('head'); badge = shadow.getElementById('badge'); minBtn = shadow.getElementById('min'); var btns = {}; speeds.forEach(function(r){ var b = document.createElement('div'); b.className = 'btn'; b.textContent = r + 'x'; b.addEventListener('click', function(){ setSpeed(r); for (var k in btns) btns[k].classList.remove('active'); b.classList.add('active'); }); row.appendChild(b); btns[r] = b; }); shadow.getElementById('off').addEventListener('click', function(){ forceAutoRate = false; stopRateKeeper(); broadcastRate(-1); unlockVideos(); for (var k in btns) btns[k].classList.remove('active'); updateBadge(); }); // 缩小 / 展开 minBtn.addEventListener('click', function(e){ e.stopPropagation(); var col = box.classList.toggle('collapsed'); minBtn.textContent = col ? '+' : '-'; }); // 隐藏为右下角小圆标 shadow.getElementById('close').addEventListener('click', function(e){ e.stopPropagation(); box.style.display = 'none'; badge.style.display = 'flex'; updateBadge(); }); badge.addEventListener('click', function(){ box.style.display = 'block'; badge.style.display = 'none'; }); var dragging = false, sx, sy, il, it; head.addEventListener('pointerdown', function(e){ if (e.target && e.target.closest && e.target.closest('.ctrl')) return; dragging = true; var rect = root.getBoundingClientRect(); il = rect.left; it = rect.top; sx = e.clientX; sy = e.clientY; root.style.left = il + 'px'; root.style.top = it + 'px'; root.style.right = 'auto'; try { head.setPointerCapture(e.pointerId); } catch (err) {} e.preventDefault(); }); head.addEventListener('pointermove', function(e){ if (!dragging) return; var dx = e.clientX - sx, dy = e.clientY - sy; var nl = Math.max(0, Math.min(window.innerWidth - 60, il + dx)); var nt = Math.max(0, Math.min(window.innerHeight - 40, it + dy)); root.style.left = nl + 'px'; root.style.top = nt + 'px'; e.preventDefault(); }); function endDrag(){ dragging = false; } head.addEventListener('pointerup', endDrag); head.addEventListener('pointercancel', endDrag); } initForceSpeed(); })();