// ==UserScript==
// @name speedup
// @namespace https://github.com/echowav
// @version 0.0.3
// @description 快速视频播放脚本,支持长按空格键加速视频播放
// @description:en Speed up video playback with long press on space
// @author echowav
// @match *://*/*
// @icon null
// @grant none
// @license MIT
// ==/UserScript==
(function () {
function useLongPress(target, {
onStart = () => {},
onHold = null,
onLongPress = () => {},
onClick = () => {},
onRelease = () => {},
onReleaseAfterLong = () => {},
duration = 800,
holdInterval = 100,
preventDefault = false,
capture = false,
disabled = () => true,
} = {}) {
let isPressed = false;
let longPressTriggered = false;
let longPressTimer = null;
let holdIntervalTimer = null;
let totalHoldTime = 0;
const clear = (event) => {
if (disabled(event)) return;
if (!isPressed) return;
isPressed = false;
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
if (holdIntervalTimer) {
clearInterval(holdIntervalTimer);
holdIntervalTimer = null;
}
onRelease(event);
if (longPressTriggered) {
onReleaseAfterLong(event);
} else {
onClick(event);
}
longPressTriggered = false;
};
const start = (event) => {
if (disabled(event)) return;
if (preventDefault) event.preventDefault();
if (isPressed) return;
isPressed = true;
longPressTriggered = false;
totalHoldTime = 0;
onStart(event);
// 设置长按判定
longPressTimer = setTimeout(() => {
if (isPressed && !longPressTriggered) {
longPressTriggered = true;
onLongPress(event);
}
}, duration);
// 设置持续 onHold
if (onHold) {
holdIntervalTimer = setInterval(() => {
if (isPressed) {
totalHoldTime += holdInterval;
onHold(event, { duration: totalHoldTime, triggered: longPressTriggered });
}
}, holdInterval);
}
};
// ========== 工具:统一添加事件并返回解绑函数 ==========
const addEvent = (target, type, handler) => {
target.addEventListener(type, handler, { capture });
return () => target.removeEventListener(type, handler, { capture });
};
// ========== 情况 1:DOM 元素(鼠标 + 触摸)==========
if (target instanceof Element) {
const cleanupFns = [];
// 主事件
cleanupFns.push(addEvent(target, 'mousedown', start));
cleanupFns.push(addEvent(target, 'touchstart', start));
// 全局释放事件(必须绑定到 document,也应支持 capture)
cleanupFns.push(addEvent(document, 'mouseup', clear));
cleanupFns.push(addEvent(document, 'touchend', clear));
cleanupFns.push(addEvent(document, 'touchcancel', clear));
return () => {
cleanupFns.forEach(fn => fn());
};
}
// ========== 情况 2:键盘按键 ==========
else if (typeof target === 'string') {
const keyDownHandler = (e) => {
if (disabled(e)) return;
if (e.code === target) {
if (!isPressed) {
start(e);
}
if (isPressed && e.repeat) {
// 如果是重复按下,直接忽略
e.stopPropagation();
e.preventDefault();
return;
}
}
};
const keyUpHandler = (e) => {
if (disabled(e)) return;
if (e.code === target && isPressed) {
clear(e);
}
};
const cleanupKeydown = addEvent(document, 'keydown', keyDownHandler);
const cleanupKeyup = addEvent(document, 'keyup', keyUpHandler);
return () => {
cleanupKeydown();
cleanupKeyup();
};
}
console.warn('useLongPress: Unsupported target type');
return () => {};
}
let video = null;
let isSpeedUp = false;
let speedIndicator = null;
const LONG_PRESS_DURATION = 500;
function createSpeedIndicator() {
if (speedIndicator) return;
speedIndicator = document.createElement('div');
speedIndicator.innerHTML = `
<span>2x</span>
<div class="triangle-container">
<div class="triangle"></div>
<div class="triangle" style="margin-left: 2px;"></div>
</div>
`;
speedIndicator.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #fffe;
height: 40px;
box-sizing: border-box;
padding: 10px 20px;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
z-index: 9999;
display: none;
pointer-events: none;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
display: flex;
align-items: center;
gap: 8px;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes fadeInOut {
0% { opacity: 0.4; }
50% { opacity: 0.9; }
100% { opacity: 0.4; }
}
.triangle-container {
display: flex;
}
.triangle {
width: 0;
height: 0;
border-left: 8px solid #fff;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
}
.triangle:nth-child(1) {
animation: fadeInOut 1s ease-in-out infinite;
animation-delay: -0.25s;
}
.triangle:nth-child(2) {
animation: fadeInOut 1s ease-in-out infinite;
}
`;
document.head.appendChild(style);
}
function showSpeedIndicator() {
if (!video) return;
if (!speedIndicator) {
createSpeedIndicator();
}
if (speedIndicator && !speedIndicator.parentNode) {
video.parentNode?.appendChild(speedIndicator);
}
updateIndicatorPosition();
if (speedIndicator) {
speedIndicator.style.display = 'flex';
}
}
function updateIndicatorPosition() {
if (!speedIndicator || !video) return;
const videoRect = video.getBoundingClientRect();
const leftOffset = videoRect.left + videoRect.width / 2;
speedIndicator.style.left = `${leftOffset}px`;
speedIndicator.style.top = `${videoRect.top + 20}px`;
}
function hideSpeedIndicator() {
if (speedIndicator) {
speedIndicator.style.display = 'none';
}
}
function findVideo() {
return document.querySelector('video');
}
function restoreNormalSpeed() {
if (!video) return;
isSpeedUp = false;
video.playbackRate = 1.0;
hideSpeedIndicator();
}
function speedUp() {
if (!video) return;
isSpeedUp = true;
video.playbackRate = 2.0;
showSpeedIndicator();
}
window.addEventListener('scroll', () => {
if (isSpeedUp && video) {
updateIndicatorPosition();
}
});
window.addEventListener('resize', () => {
if (isSpeedUp && video) {
updateIndicatorPosition();
}
});
// DOMContentLoaded 后监听新元素插入(如动态加载的视频)
document.addEventListener('DOMContentLoaded', () => {
const observer = new MutationObserver(() => {
if (!video) {
video = findVideo();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
// 监听 DOM 变化以更新指示器位置(如样式或类名改变影响布局)
const positionObserver = new MutationObserver(() => {
if (isSpeedUp && video) {
updateIndicatorPosition();
}
});
positionObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
function togglePlayPause() {
if (!video) return;
if (video.paused) {
video.play();
} else {
video.pause();
}
}
const isInputArea = (e) => {
const target = e.target;
return /input|textarea/i.test(target.tagName) ||
target.contentEditable === 'true' ||
target.isContentEditable;
}
useLongPress('Space', {
onStart: (e) => {
e.preventDefault();
e.stopPropagation();
video = findVideo();
},
onLongPress: () => {
speedUp();
},
onRelease: (e) => {
e.preventDefault();
e.stopPropagation();
restoreNormalSpeed();
},
onClick: () => {
togglePlayPause();
},
disabled: isInputArea,
preventDefault: true,
capture: true,
duration: LONG_PRESS_DURATION,
})
})();