speedup

快速视频播放脚本,支持长按空格键加速视频播放

当前为 2025-08-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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,
})
})();