Greasy Fork is available in English.
为YouTube等默认需要长按鼠标加速的视频网站增加长按方向键加速|Add keyboard long‑press speed boost for video sites like YouTube that normally require holding the mouse button to accelerate.
// ==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/*
// ==/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;
// 通过监听<title>的文本内容变化判断页面切换(轻量级)
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();
})();