Greasy Fork

视频倍速播放

长按右方向键倍速播放,松开恢复原速,按 + 键增加倍速,按 - 键减少倍速,单击右方向键快进5秒。 按 ] 键从 1.5 倍速开始,每按一次增加 0.5 倍速,按 [ 键每次减少 0.5 倍速,按P键恢复1.0倍速。适配大部分网页播放器,尤其适配jellyfin等播放器播放nas内容。

// ==UserScript==
// @name         视频倍速播放
// @namespace    http://tampermonkey.net/
// @version      1.3.4
// @description  长按右方向键倍速播放,松开恢复原速,按 + 键增加倍速,按 - 键减少倍速,单击右方向键快进5秒。 按 ] 键从 1.5 倍速开始,每按一次增加 0.5 倍速,按 [ 键每次减少 0.5 倍速,按P键恢复1.0倍速。适配大部分网页播放器,尤其适配jellyfin等播放器播放nas内容。
// @license MIT
// @author       diyun
// @include      http://*/*
// @include      https://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
// @grant        none
// ==/UserScript==
(function () {
  "use strict";
  let currentUrl = location.href;
  let keydownListener = null;
  let keyupListener = null;
  let urlObserver = null;
  let videoObserver = null;  // 添加此行
  let videoChangeObserver = null;
  let activeObservers = new Set();
  // 完整的清理函数
  function cleanup() {
    // 清理所有事件监听器
    if (keydownListener) {
      document.removeEventListener("keydown", keydownListener, true);
      keydownListener = null;
    }
    if (keyupListener) {
      document.removeEventListener("keyup", keyupListener, true);
      keyupListener = null;
    }
    // 清理所有观察器
    activeObservers.forEach((observer) => {
      if (observer && observer.disconnect) {
        observer.disconnect();
      }
    });
    activeObservers.clear();
    videoObserver = null;
    urlObserver = null;
    videoChangeObserver = null;
  }
  // 等待视频元素加载
  function waitForVideoElement() {
    return new Promise((resolve, reject) => {
      const maxAttempts = 10;
      let attempts = 0;
      const checkVideo = () => {
        const video = document.querySelector("video");
        if (video && video.readyState >= 1) {
          return video;
        }
        return null;
      };
      // 立即检查
      const video = checkVideo();
      if (video) {
        resolve(video);
        return;
      }
      // 创建观察器
      const observer = new MutationObserver(() => {
        attempts++;
        const video = checkVideo();
        if (video) {
          observer.disconnect();
          resolve(video);
        } else if (attempts >= maxAttempts) {
          observer.disconnect();
          console.warn("未找到视频元素,脚本已停止运行");
          reject({ type: "no_video" }); // 使用对象替代 Error
        }
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
      activeObservers.add(observer);
      // 设置超时
      setTimeout(() => {
        observer.disconnect();
        activeObservers.delete(observer);
        console.warn("等待视频元素超时,脚本已停止运行");
        reject({ type: "timeout" }); // 使用对象替代 Error
      }, 10000);
    });
  }
  // 显示浮动提示
  function showFloatingMessage(message) {
    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
      .floating-message {
        position: fixed;
        top: 10%;
        left: 50%;
        transform: translateX(-50%);
        background: rgba(0, 0, 0, 0.8);
        color: white;
        padding: 8px 16px;
        border-radius: 4px;
        z-index: 2147483647;
        pointer-events: none;
        font-size: 1.1em;
        text-align: center;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        transition: opacity 0.3s ease;
      }
    `;
    document.head.appendChild(style);

    // 清除已存在的提示
    const existingMessages = document.querySelectorAll('.floating-message');
    existingMessages.forEach(el => el.remove());

    // 创建并显示新提示
    const messageEl = document.createElement('div');
    messageEl.className = 'floating-message';
    messageEl.textContent = message;

    // 获取全屏元素或回退到body
    const fullscreenElement = document.fullscreenElement || 
                             document.webkitFullscreenElement || 
                             document.mozFullScreenElement || 
                             document.msFullscreenElement;
    
    const targetContainer = fullscreenElement || document.body;
    targetContainer.appendChild(messageEl);

    // 2秒后自动移除
    setTimeout(() => {
      if (messageEl.parentElement) {
        messageEl.remove();
      }
    }, 2000);
  }
  
  // 检查是否在可输入元素中
  function isInInputElement(event) {
    const target = event.target;
    // 检查元素是否是输入类型
    if (target.tagName === 'INPUT' || 
        target.tagName === 'TEXTAREA' || 
        target.tagName === 'SELECT' || 
        target.isContentEditable) {
      return true;
    }
    
    // 检查元素是否在编辑器中 (常见的编辑器包含这些类名或ID)
    const editorElements = ['editor', 'ace_editor', 'monaco-editor', 'CodeMirror'];
    for (const className of editorElements) {
      if (target.closest(`.${className}`) || target.closest(`#${className}`)) {
        return true;
      }
    }
    
    return false;
  }
  
  // 初始化脚本
  async function init() {
    cleanup();
    try {
      const video = await waitForVideoElement();
      console.log("找到视频元素:", video);
      const key = "ArrowRight"; // 监听的按键
      const increaseKey = "Equal"; // + 键
      const decreaseKey = "Minus"; // - 键
      const quickIncreaseKey = "BracketRight"; // 】键
      const quickDecreaseKey = "BracketLeft"; // 【键
      const resetSpeedKey = "KeyP"; // P键
      let targetRate = 2; // 目标倍速
      let currentQuickRate = 1.0; // 当前快速倍速
      let keyDownTime = 0; // 添加按键开始时间记录
      let originalRate = video.playbackRate; // 保存原始播放速度
      let isSpeedUp = false; // 添加一个标记来跟踪是否处于加速状态
      // 监听视频元素变化
      if (video.parentElement) {
        videoChangeObserver = new MutationObserver((mutations) => {
          const hasVideoChanges = mutations.some(
            (mutation) =>
              Array.from(mutation.removedNodes).some(
                (node) => node.tagName === "VIDEO"
              ) ||
              Array.from(mutation.addedNodes).some(
                (node) => node.tagName === "VIDEO"
              )
          );
          if (hasVideoChanges) {
            console.log("视频元素变化,重新初始化");
            cleanup();
            init().catch(console.error);
          }
        });
        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();
          }
          
          // 如果按下超过300ms,认为是长按,进入加速模式
          if (!isSpeedUp && Date.now() - keyDownTime > 300) {
            isSpeedUp = true;
            originalRate = video.playbackRate;
            video.playbackRate = targetRate;
            showFloatingMessage(`开始 ${targetRate} 倍速播放`);
          }
        }
        // 按】键增加当前播放倍速
        if (e.code === quickIncreaseKey) {
          e.preventDefault();
          e.stopImmediatePropagation();
          if (currentQuickRate === 1.0) {
            currentQuickRate = 1.5;
          } else {
            currentQuickRate += 0.5;
          }
          video.playbackRate = currentQuickRate;
          showFloatingMessage(`当前播放速度:${currentQuickRate}x`);
        }
        // 按【键减少当前播放倍速
        if (e.code === quickDecreaseKey) {
          e.preventDefault();
          e.stopImmediatePropagation();
          if (currentQuickRate > 0.5) {
            currentQuickRate -= 0.5;
            video.playbackRate = currentQuickRate;
            showFloatingMessage(`当前播放速度:${currentQuickRate}x`);
          }
        }
        // 按P键恢复1.0倍速
        if (e.code === resetSpeedKey || e.key.toLowerCase() === "p") {
          e.preventDefault();
          e.stopImmediatePropagation();
          currentQuickRate = 1.0;
          video.playbackRate = 1.0;
          showFloatingMessage("恢复正常播放速度");
        }
        // 按 + 键:增加 targetRate 的值
        if (e.code === increaseKey) {
          e.preventDefault();
          e.stopImmediatePropagation();
          targetRate += 0.5;
          showFloatingMessage(`下次倍速:${targetRate}`);
        }
        // 按 - 键:减少 targetRate 的值
        if (e.code === decreaseKey) {
          e.preventDefault();
          e.stopImmediatePropagation();
          if (targetRate > 0.5) {
            targetRate -= 0.5;
            showFloatingMessage(`下次倍速:${targetRate}`);
          } else {
            showFloatingMessage("倍速已达到最小值 0.5");
          }
        }
      };
      keyupListener = (e) => {
        // 首先检查是否在输入元素中,如果是则不处理快捷键
        if (isInInputElement(e)) {
          return;
        }
        
        if (e.code === key) {
          e.preventDefault();
          e.stopImmediatePropagation();
          
          const pressTime = Date.now() - keyDownTime;
          
          // 如果按下时间小于300ms,认为是点击,快进5秒
          if (pressTime < 300) {
            video.currentTime += 5;
          }
          
          // 如果处于加速状态,恢复原速
          if (isSpeedUp) {
            video.playbackRate = originalRate;
            showFloatingMessage(`恢复 ${originalRate} 倍速播放`);
            isSpeedUp = false;
          }
          
          // 重置状态
          keyDownTime = 0;
        }
      };
      // 绑定事件监听器
      document.addEventListener("keydown", keydownListener, true);
      document.addEventListener("keyup", keyupListener, true);
      return true;
    } catch (error) {
      console.error("初始化失败:", error);
      return false;
    }
  }
  // 监听 URL 变化
  function watchUrlChange() {
    urlObserver = new MutationObserver(() => {
      if (location.href !== currentUrl) {
        currentUrl = location.href;
        console.log("URL变化,重新初始化");
        cleanup();
        setTimeout(() => init().catch(console.error), 1000);
      }
    });
    urlObserver.observe(document.body, {
      childList: true,
      subtree: true,
    });
    activeObservers.add(urlObserver);
    // 增强的 History API 监听
    const handleStateChange = () => {
      if (location.href !== currentUrl) {
        currentUrl = location.href;
        cleanup();
        setTimeout(() => init().catch(console.error), 1000);
      }
    };
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;
    history.pushState = function () {
      originalPushState.apply(this, arguments);
      handleStateChange();
    };
    history.replaceState = function () {
      originalReplaceState.apply(this, arguments);
      handleStateChange();
    };
    window.addEventListener("popstate", handleStateChange);
  }
  // 启动脚本
  const startScript = async () => {
    let retryCount = 0;
    const maxRetries = 3;
    const tryInit = async () => {
      try {
        const success = await init();
        if (success) {
          watchUrlChange();
        } else if (retryCount < maxRetries) {
          retryCount++;
          console.warn(`初始化重试 (${retryCount}/${maxRetries})`); // 改为警告
          setTimeout(tryInit, 2000);
        }
      } catch (error) {
        // 检查错误类型
        if (error && (error.type === "no_video" || error.type === "timeout")) {
          return; // 直接返回,不做额外处理
        }
        console.warn("启动失败:", error);
        if (retryCount < maxRetries) {
          retryCount++;
          setTimeout(tryInit, 2000);
        }
      }
    };
    tryInit();
  };
  startScript();
})();