Greasy Fork

来自缓存

Greasy Fork is available in English.

更好的视频倍速|Better video speed

为YouTube等默认需要长按鼠标加速的视频网站增加长按方向键加速|Add keyboard long‑press speed boost for video sites like YouTube that normally require holding the mouse button to accelerate.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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