// ==UserScript== // @name 更好的视频倍速|Better video speed // @namespace http://tampermonkey.net/ // @version 1.5.9 // @description 为YouTube等默认需要长按鼠标加速的视频网站增加长按方向键加速|Add keyboard long‑press speed boost for video sites like YouTube that normally require holding the mouse button to accelerate. // @license MIT // @author zmabin // @include http://*/* // @include https://*/* // @exclude *://*.bilibili.com/* // @downloadURL https://update.greasyfork.icu/scripts/575987/%E6%9B%B4%E5%A5%BD%E7%9A%84%E8%A7%86%E9%A2%91%E5%80%8D%E9%80%9F%7CBetter%20video%20speed.user.js // @updateURL https://update.greasyfork.icu/scripts/575987/%E6%9B%B4%E5%A5%BD%E7%9A%84%E8%A7%86%E9%A2%91%E5%80%8D%E9%80%9F%7CBetter%20video%20speed.meta.js // ==/UserScript== (function () { "use strict"; let keydownListener = null; let keyupListener = null; let titleObserver = null; // 监听标题变化 let videoElementObserver = null; // 等待视频出现 let videoChangeObserver = null; // 监听当前视频被移除 let speedIndicator = null; let currentVideo = null; let indicatorParentOriginalPosition = null; let initPromise = null; let ytNavigateListener = null; let activeObservers = new WeakSet(); // 追踪观察器便于清理 // ---------- 清理所有监听和界面 ---------- function fullCleanup() { // 键盘事件 if (keydownListener) { document.removeEventListener("keydown", keydownListener, true); keydownListener = null; } if (keyupListener) { document.removeEventListener("keyup", keyupListener, true); keyupListener = null; } // 所有观察器 [titleObserver, videoElementObserver, videoChangeObserver].forEach(obs => { if (obs) { obs.disconnect(); activeObservers.delete(obs); } }); titleObserver = null; videoElementObserver = null; videoChangeObserver = null; removeSpeedIndicator(); if (window.__videoSpeedStyleEl) { window.__videoSpeedStyleEl.remove(); delete window.__videoSpeedStyleEl; } } // ---------- 倍速指示器 ---------- function showSpeedIndicator(rate, video) { removeSpeedIndicator(); currentVideo = video; const parent = video.parentNode; if (!parent) return; const computedStyle = window.getComputedStyle(parent); if (computedStyle.position === "static") { indicatorParentOriginalPosition = "static"; parent.style.position = "relative"; } else { indicatorParentOriginalPosition = null; } if (!document.getElementById("video-speed-anim-style")) { const styleEl = document.createElement("style"); styleEl.id = "video-speed-anim-style"; styleEl.textContent = ` @keyframes breathe { 0%, 100% { opacity: 0.2; } 50% { opacity: 1; } } .video-speed-indicator .triangle { display: inline-block; font-size: 10px; color: #fff; margin-right: 0; animation: breathe 1.2s ease-in-out infinite; } .video-speed-indicator .triangle:nth-child(1) { animation-delay: 0s; } .video-speed-indicator .triangle:nth-child(2) { animation-delay: 0.15s; } .video-speed-indicator .triangle:nth-child(3) { animation-delay: 0.3s; } `; document.head.appendChild(styleEl); window.__videoSpeedStyleEl = styleEl; } speedIndicator = document.createElement("div"); speedIndicator.className = "video-speed-indicator"; Object.assign(speedIndicator.style, { position: "absolute", left: "50%", transform: "translateX(-50%)", background: "rgba(0,0,0,0.75)", color: "#fff", padding: "4px 16px", borderRadius: "4px", zIndex: "2147483647", pointerEvents: "none", display: "flex", alignItems: "center", gap: "2px", whiteSpace: "nowrap", fontFamily: "Arial, sans-serif", border: "none" }); for (let i = 0; i < 3; i++) { const tri = document.createElement("span"); tri.className = "triangle"; tri.textContent = "▶"; speedIndicator.appendChild(tri); } const text = document.createElement("span"); text.className = "speed-text"; text.textContent = `${rate.toFixed(1)}x 倍速播放中`; text.style.fontSize = "14px"; text.style.fontWeight = "500"; speedIndicator.appendChild(text); parent.appendChild(speedIndicator); const videoHeight = video.offsetHeight || video.clientHeight || 0; const topOffset = Math.max(videoHeight * 0.04, 8); speedIndicator.style.top = topOffset + "px"; } function updateSpeedIndicator(rate) { const textEl = speedIndicator?.querySelector(".speed-text"); if (textEl) textEl.textContent = `${rate.toFixed(1)}x 倍速播放中`; } function removeSpeedIndicator() { if (speedIndicator) { if (indicatorParentOriginalPosition === "static" && speedIndicator.parentNode) { speedIndicator.parentNode.style.position = ""; } speedIndicator.remove(); speedIndicator = null; indicatorParentOriginalPosition = null; } } // ---------- 等待有效视频元素(纯观察,无轮询)---------- function waitForVideoElement() { return new Promise((resolve) => { let observer = null; let currentTarget = null; function cleanObserver() { if (observer) { observer.disconnect(); activeObservers.delete(observer); observer = null; } } function foundVideo(video) { cleanObserver(); resolve(video); } function handleVideo(video) { if (!video || video === currentTarget) return; currentTarget = video; if (video.readyState >= 1) { foundVideo(video); } else { // 等待元数据加载 const onMeta = () => { video.removeEventListener('loadedmetadata', onMeta); if (document.contains(video)) { foundVideo(video); } else { // 视频节点被移除了,重新搜索 currentTarget = null; startObserving(); } }; video.addEventListener('loadedmetadata', onMeta, { once: true }); // 若 30 秒仍未加载元数据,放弃并重新搜索 const timeout = setTimeout(() => { video.removeEventListener('loadedmetadata', onMeta); currentTarget = null; startObserving(); }, 30000); // 清理函数 const originalClean = cleanObserver; cleanObserver = () => { clearTimeout(timeout); originalClean(); }; } } function checkAndObserve() { const video = document.querySelector("video"); if (video) { handleVideo(video); return true; } return false; } function startObserving() { cleanObserver(); if (checkAndObserve()) return; // 使用 MutationObserver 等待视频出现(不再用 setInterval) observer = new MutationObserver(() => { if (checkAndObserve()) { cleanObserver(); } }); // 确保 body 已存在 const target = document.body || document.documentElement; if (target) { observer.observe(target, { childList: true, subtree: true }); activeObservers.add(observer); } else { document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); activeObservers.add(observer); checkAndObserve(); // 再次检查 }, { once: true }); } } startObserving(); }); } // ---------- 判断是否在输入框内 ---------- function isInInputElement(event) { const target = event.target; if (target.isContentEditable) return true; const tag = target.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true; // 常见代码编辑器 const cls = target.className || ""; if (/editor|ace_editor|monaco-editor|CodeMirror/i.test(cls)) return true; return false; } // ---------- 核心初始化 ---------- async function setupForVideo(video) { // 绑定键盘控制 const KEY = "ArrowRight"; const ADD_KEY = "NumpadAdd"; const SUB_KEY = "NumpadSubtract"; const RESET_KEY = "KeyP"; let targetRate = 3; const MAX_RATE = 16; const MIN_RATE = 0.5; let keyDownTime = 0; let originalRate = video.playbackRate; let isSpeedUp = false; // 观察当前视频是否被移除或替换 if (video.parentElement) { videoChangeObserver = new MutationObserver((mutations) => { const videoChanged = mutations.some(mutation => { return Array.from(mutation.removedNodes).some(node => node.nodeName === "VIDEO") || Array.from(mutation.addedNodes).some(node => node.nodeName === "VIDEO"); }); if (videoChanged) { console.log("视频元素变化,重新初始化"); fullCleanup(); initScript(); } }); videoChangeObserver.observe(video.parentElement, { childList: true, subtree: true }); activeObservers.add(videoChangeObserver); } // 键盘按下 keydownListener = (e) => { if (isInInputElement(e)) return; if (e.code === KEY) { e.preventDefault(); e.stopImmediatePropagation(); if (!keyDownTime) keyDownTime = Date.now(); if (!isSpeedUp && Date.now() - keyDownTime > 300) { isSpeedUp = true; originalRate = video.playbackRate; video.playbackRate = targetRate; showSpeedIndicator(targetRate, video); } return; } if (e.code === ADD_KEY) { e.preventDefault(); e.stopImmediatePropagation(); if (targetRate < MAX_RATE) { targetRate += 0.5; if (isSpeedUp) { video.playbackRate = targetRate; updateSpeedIndicator(targetRate); } } return; } if (e.code === SUB_KEY) { e.preventDefault(); e.stopImmediatePropagation(); if (targetRate > MIN_RATE) { targetRate -= 0.5; if (isSpeedUp) { video.playbackRate = targetRate; updateSpeedIndicator(targetRate); } } return; } if (e.code === RESET_KEY) { e.preventDefault(); e.stopImmediatePropagation(); targetRate = 3; if (isSpeedUp) { video.playbackRate = targetRate; updateSpeedIndicator(targetRate); } } }; // 键盘松开 keyupListener = (e) => { if (isInInputElement(e)) return; if (e.code === KEY) { e.preventDefault(); e.stopImmediatePropagation(); const pressDuration = Date.now() - keyDownTime; if (pressDuration < 300) { video.currentTime += 5; // 短按快进5秒 } if (isSpeedUp) { video.playbackRate = originalRate; removeSpeedIndicator(); isSpeedUp = false; } keyDownTime = 0; } }; document.addEventListener("keydown", keydownListener, true); document.addEventListener("keyup", keyupListener, true); } async function initScript() { fullCleanup(); try { const video = await waitForVideoElement(); console.log("找到视频元素:", video); await setupForVideo(video); } catch (err) { console.error("初始化失败:", err); } } // ---------- 监听 URL / 页面变化 ---------- function enableTitleWatcher() { const titleEl = document.querySelector("title"); if (!titleEl) return; // 通过监听的文本内容变化判断页面切换(轻量级) titleObserver = new MutationObserver(() => { // 简单比较 URL 是否真的变了(防止 title 其他属性变化导致误触发) const newHref = location.href; if (newHref !== titleObserver._lastHref) { titleObserver._lastHref = newHref; console.log("title 变化,可能跳转了页面"); fullCleanup(); initScript(); } }); titleObserver.observe(titleEl, { characterData: true, childList: true, subtree: true }); titleObserver._lastHref = location.href; activeObservers.add(titleObserver); } function watchPageChanges() { // 1. 劫持 history 方法(pushState / replaceState) const origPush = history.pushState; const origReplace = history.replaceState; history.pushState = function () { origPush.apply(this, arguments); onPotentialNavigate(); }; history.replaceState = function () { origReplace.apply(this, arguments); onPotentialNavigate(); }; window.addEventListener("popstate", onPotentialNavigate); // 2. 对 YouTube 特殊事件 if (location.hostname.includes("youtube.com")) { ytNavigateListener = () => { console.log("YouTube 导航事件触发"); fullCleanup(); initScript(); }; document.addEventListener("yt-navigate-finish", ytNavigateListener); } // 3. 标题变化作为通用 SPA 检测 enableTitleWatcher(); function onPotentialNavigate() { // 轻微延迟,确保 DOM 更新完成 setTimeout(() => { if (location.href !== (titleObserver?._lastHref || "")) { if (titleObserver) titleObserver._lastHref = location.href; fullCleanup(); initScript(); } }, 100); } } // ---------- 启动入口 ---------- function start() { if (document.body) { initScript(); watchPageChanges(); } else { document.addEventListener("DOMContentLoaded", () => { initScript(); watchPageChanges(); }, { once: true }); } } start(); })();