Greasy Fork

Greasy Fork is available in English.

全能视频播放器速度控制(最大16倍速)

支持【B站】【爱奇艺】【腾讯视频】【优酷】...等网站

目前为 2025-04-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         全能视频播放器速度控制(最大16倍速)
// @namespace    http://tampermonkey.net/
// @version      3.7
// @description  支持【B站】【爱奇艺】【腾讯视频】【优酷】...等网站
// @author       不会起名
// @match         *://*blog.csdn.net/*
// @match         *://*download.csdn.net/*
// @match         *://*c.pc.qq.com/middlem*
// @match         *://*pan.baidu.com/disk/main*
// @match         *://link.csdn.net/*
// @match         *://link.zhihu.com/*
// @match         *://browser.gwdang.com/*
// @match         *://*www.jianshu.com/go-wild*
// @match         *://*gitee.com/link*
// @match         *://*juejin.cn/?target*
// @match         *://www.aliyundrive.com/drive*
// @match         *://www.alipan.com/drive/*
// @match         *://*.youtube.com/watch?v=*
// @match         *://support.qq.com/products*
// @match         *://weibo.cn/sinaurl*
// @match         *://afdian.net/link*
// @match         *://*oschina.net/action/GoToLink*
// @match         *://jump2.bdimg.com/safecheck*
// @match         *://www.douban.com/link2/?url*
// @match         *://link.17173.com*
// @match         *://search.suning.com/*
// @match         *://pan.quark.cn/*
// @match         *://docs.qq.com/scenario/link*
// @match         *://mail.qq.com/cgi-bin/readtemplate*
// @match         *://cloud.tencent.com/developer/tools/blog-entry*
// @match         *://link.uisdc.com/*
// @match         *://*.tudou.com/listplay/*
// @match         *://*.tudou.com/albumplay/*
// @match         *://*.tudou.com/programs/view/*
// @match         *://*.tudou.com/v*
// @match         *://*.mgtv.com/b/*
// @match         *://film.sohu.com/album/*
// @match         *://tv.sohu.com/v/*
// @match         *://*.acfun.cn/v/*
// @match         *://*.bilibili.com/video/*
// @match         *://*.bilibili.com/anime/*
// @match         *://*.bilibili.com/bangumi/play/*
// @match         *://*.pptv.com/show/*
// @match         *://*.baofeng.com/play/*
// @match         *://*.wasu.cn/Play/show*
// @match         *://v.yinyuetai.com/video/*
// @match         *://v.yinyuetai.com/playlist/*
// @match         *://*.wasu.cn/Play/show/*
// @match         *://music.taihe.com/song*
// @match         *://music.163.com/song*
// @match         *://music.163.com/m/song*
// @match         *://y.qq.com/*
// @match         *://*.kugou.com/*
// @match         *://*.kuwo.cn/*
// @match         *://*.xiami.com/*
// @match         *://music.taihe.com/*
// @match         *://*.1ting.com/player*
// @match         *://www.qingting.fm/*
// @match         *://www.lizhi.fm/*
// @match         *://music.migu.cn/*
// @match         *://www.shangxueba.com/ask/*.html
// @match         *://www.ximalaya.com/*
// @match         *://www.shangxueba.com/ask/*.html
// @match         *://pan.baidu.com/disk/home*
// @match         *://yun.baidu.com/disk/home*
// @match         *://pan.baidu.com/s/*
// @match         *://yun.baidu.com/s/*
// @match         *://pan.baidu.com/share/link*
// @match         *://yun.baidu.com/share/link*
// @match         *://wenku.baidu.com/view/*
// @match         *://settings.wandhi.com/*
// @match         *://m.youku.com/v*
// @match         *://m.youku.com/a*
// @match         *://v.youku.com/v_*
// @match         *://v.youku.com/pad_show*
// @match         *://*.iqiyi.com/v_*
// @match         *://*.iqiyi.com/w_*
// @match         *://*.iqiyi.com/a_*
// @match         *://*.iqiyi.com/adv*
// @match         *://*.iq.com/play/*
// @match         *://*.le.com/ptv/vplay/*
// @match         *://v.qq.com/x/cover/*
// @match         *://v.qq.com/x/page/*
// @match         *://v.qq.com/*play*
// @match         *://v.qq.com/cover*
// @match         *://c.pc.qq.com/ios*
// @match         *://www.v2ex.com/t/*
// @match         *://*.nodeseek.com/jump*
// @match         *://*.zhihu.com/question*
// @match         *://www.baidu.com/*
// @match         *://www.google.com/*
// @match         *://www.sogou.com/*
// @match         *://www.so.com/s*
// @match         *://cn.bing.com/search*
// @match         *://sspai.com/link*
// @match         *://*.kdocs.cn/office/link*
// @match         *://ispacesoft.com/*.html
// @match         *://tv.wandhi.com/go.html*
// @match         *://tv.wandhi.com/check.html
// @match         *://*.xiaohongshu.com/explore*
// @match         *://www.yuque.com/r/goto*
// @match         *://blog.51cto.com/transfer*
// @match         *://r.wjx.com/redirect.aspx*
// @match         *://www.infoq.cn/link*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ================
  // 全局CSS变量
  // ================
  document.documentElement.style.setProperty('--greyLightText', '#9baacf');
  document.documentElement.style.setProperty('--greyLightBg', '#e4ebf5');
  document.documentElement.style.setProperty('--greyLightShadow1', '#c8d0e7');
  document.documentElement.style.setProperty('--greyLightShadow2', '#fff');

  document.documentElement.style.setProperty('--greyDarkText', 'white');
  document.documentElement.style.setProperty('--greyDarkBg', '#696969');
  document.documentElement.style.setProperty('--greyDarkShadow1', '#595959');
  document.documentElement.style.setProperty('--greyDarkShadow2', '#797979');

  // ================
  // 配置和常量
  // ================
  const CONFIG = {
    pos: GM_getValue('controlPos', { x: 20, y: 20 }),
    isCollapsed: GM_getValue('isCollapsed', false),
    theme: GM_getValue('theme', 'light'),
    speed: {
      min: '0.10',
      max: '16',
      step: '0.05',
      value: '1',
    }
  };

  const THEMES = {
    dark: {
      bg: 'var(--greyDarkBg)',
      text: 'var(--greyDarkText)',
      border: '#666',
      buttonBg: '#555',
      buttonText: 'var(--greyDarkText)',
      inputBg: '#333',
      boxShadow: '3px 3px 6px var(--greyDarkShadow1), -2px -2px 5px var(--greyDarkShadow2)',
      clickBoxShadow: 'inset 2px 2px 5px var(--greyDarkShadow1), inset -2px -2px 5px var(--greyDarkShadow2) !important',
      rangeSlider1: 'white',
      rangeSlider2: '#b1b1b1',
    },
    light: {
      bg: 'var(--greyLightBg)',
      text: 'var(--greyLightText)',
      border: '#ddd',
      buttonBg: '#eee',
      buttonText: 'var(--greyLightText)',
      inputBg: '#fff',
      boxShadow: '3px 3px 6px var(--greyLightShadow1), -2px -2px 5px var(--greyLightShadow2)',
      clickBoxShadow: 'inset 2px 2px 5px var(--greyLightShadow1), inset -2px -2px 5px var(--greyLightShadow2) !important',
      rangeSlider1: 'white',
      rangeSlider2: 'var(--greyLightText)',
    },
  };

  // ================
  // 全局变量
  // ================
  let video = null;
  let isDragging = false;
  let startX, startY, initLeft, initTop;

  // ================
  // DOM 元素
  // ================
  const controls = createControls();
  const header = createHeader();

  const speedDisplay = document.createElement('span');
  const speedSlider = document.createElement('input');
  const numInput = document.createElement('input');
  const content = createContent();

  // ================
  // 动态创建 CSS 类
  // ================
  const style123 = document.createElement('style');
  style123.textContent = '#bili-speed-control .lightBtn:active{box-shadow:' + THEMES[CONFIG.theme].clickBoxShadow + '}#bili-speed-control .darkBtn:active{box-shadow:' + THEMES[CONFIG.theme].clickBoxShadow + '}';
  document.head.appendChild(style123);

  const styleRange = document.createElement('style');
  styleRange.textContent = `
  #bili-speed-control {
    position: fixed;
    z-index: 9999;
    border-radius: 5px;
    cursor: move;
    user-select: none;
    transition: all 0.3s ease;
  }
  #bili-speed-control .BSC_header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  #bili-speed-control .btnConfig {
    padding: 2px 8px;
    border-radius: 3px;
    background: transparent;
    border: none;
    cursor: pointer;
  }
  #bili-speed-control .lightBtn {

  }
  #bili-speed-control .darkBtn {
    
  }
  #bili-speed-control .slider {
    --slider-width: 100%;
    --slider-height: 6px;
    --slider-border-radius: 999px;
    --level-transition-duration: .1s;
  }
  #bili-speed-control .slider {
    display: flex;
    align-items: center;
    cursor: pointer;
  }
  #bili-speed-control .slider .level {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    width: var(--slider-width);
    height: var(--slider-height);
    background: var(--slider-bg);
    overflow: hidden;
    border-radius: var(--slider-border-radius);
    -webkit-transition: height var(--level-transition-duration);
    -o-transition: height var(--level-transition-duration);
    transition: height var(--level-transition-duration);
    cursor: pointer;
  }
  #bili-speed-control .level::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 0;
    height: 0;
    -webkit-box-shadow: -200px 0 0 200px var(--level-color);
    box-shadow: -200px 0 0 200px var(--level-color);
  }
  #bili-speed-control .slider:hover .level {
    height: calc(var(--slider-height) * 2);
  }
  #bili-speed-control .numInputSpeed {
    width: 50px;
    margin-left: 10px;
    padding: 3px 6px;
    border-radius: 4px;
    border: none;
    background: transparent;
  }
  `;
  document.head.appendChild(styleRange);

  // ================
  // 主初始化流程
  // ================
  initializeControls();

  function createControls() {
    const el = document.createElement('div');
    el.id = 'bili-speed-control';
    Object.assign(el.style, {
      padding: CONFIG.isCollapsed ? '8px' : '10px',
      width: CONFIG.isCollapsed ? '150px' : '200px',
    });
    return el;
  }

  function createHeader() {
    const header = document.createElement('div');
    header.classList.add('BSC_header');
    header.style.marginBottom = CONFIG.isCollapsed ? '0' : '10px';

    const title = document.createElement('span');
    title.textContent = '🎚️ 播放控制';

    const btnContainer = document.createElement('div');

    const toggleBtn = createButton(
      CONFIG.isCollapsed ? '▶' : '▼',
      {
        marginRight: '5px',
        boxShadow: THEMES[CONFIG.theme].boxShadow,
      },
      () => toggleCollapse()
    );

    const themeBtn = createButton(CONFIG.theme === 'dark' ? '🌞' : '🌙', { boxShadow: THEMES[CONFIG.theme].boxShadow }, () => toggleTheme());

    btnContainer.append(toggleBtn, themeBtn);
    header.append(title, btnContainer);
    return header;
  }

  function createContent() {
    const content = document.createElement('div');
    const content2 = document.createElement('div');

    Object.assign(content.style, {
      overflow: 'hidden',
      transition: 'all 0.3s ease',
      opacity: CONFIG.isCollapsed ? '0' : '1',
      maxHeight: CONFIG.isCollapsed ? '0px' : '200px',
    });

    // 预设按钮
    const presetContainer = document.createElement('div');
    presetContainer.style.marginBottom = '10px';
    [0.5, 0.65, 0.85, 1.0, 1.15, 1.25].forEach((speed) => {
      const btn = createButton(
        `${speed}x`,
        {
          margin: '3px',
          width: '60px',
          transition: 'width 0.3s ease',
        },
        () => syncInputs(speed)
      );
      presetContainer.appendChild(btn);
    });

    // 速度控制组件
    speedDisplay.textContent = '当前速度:1x';

    speedSlider.type = 'range';
    Object.assign(speedSlider, CONFIG.speed);
    speedSlider.classList.add('level');

    numInput.type = 'number';
    Object.assign(numInput, CONFIG.speed);
    numInput.classList.add('numInputSpeed')

    content.append(presetContainer, speedDisplay);
    content2.append(speedSlider, numInput);
    content2.classList.add('slider');
    content.append(content2);
    return content;
  }

  function createButton(text, style, clickHandler) {
    const btn = document.createElement('button');
    btn.textContent = text;
    btn.classList.add('btnConfig')
    Object.assign(btn.style, {
      ...style,
    });
    btn.classList.add(CONFIG.theme + 'Btn');
    btn.addEventListener('click', clickHandler);
    return btn;
  }

  // ================
  // 核心功能
  // ================
  function initializeControls() {
    controls.style.left = `${CONFIG.pos.x}px`;
    controls.style.top = `${CONFIG.pos.y}px`;
    controls.append(header, content);
    document.body.appendChild(controls);
    applyTheme();
    setupEventListeners();
  }

  function applyTheme() {
    const theme = THEMES[CONFIG.theme];

    document.documentElement.style.setProperty('--level-color', theme.rangeSlider1);
    document.documentElement.style.setProperty('--slider-bg', theme.rangeSlider2);

    Object.assign(controls.style, {
      background: theme.bg,
      color: theme.text,
      border: `1px solid ${theme.border}`,
    });

    document.querySelectorAll('#bili-speed-control button').forEach((btn) => {
      Object.assign(btn.style, {
        color: theme.buttonText,
        boxShadow: THEMES[CONFIG.theme].boxShadow,
      });
    });

    Object.assign(numInput.style, {
      boxShadow: THEMES[CONFIG.theme].clickBoxShadow,
    });
  }

  function setupEventListeners() {
    // 拖拽
    header.addEventListener('mousedown', startDrag);
    document.addEventListener('mousemove', handleDrag);
    document.addEventListener('mouseup', endDrag);

    // 速度控制
    speedSlider.addEventListener('input', (e) => syncInputs(e.target.value));
    numInput.addEventListener('change', handleNumberInput);

    // 快捷键
    document.addEventListener('keydown', handleKeyboardShortcuts);

    // 视频检测
    setTimeout(updateVideoElement, 500);
  }

  // ================
  // 功能实现
  // ================
  function toggleCollapse() {
    CONFIG.isCollapsed = !CONFIG.isCollapsed;

    // 宽度切换
    controls.style.width = CONFIG.isCollapsed ? '150px' : '200px';
    controls.style.padding = CONFIG.isCollapsed ? '8px' : '10px';

    // 内容区域切换
    content.style.maxHeight = CONFIG.isCollapsed ? '0px' : '200px';
    content.style.opacity = CONFIG.isCollapsed ? '0' : '1';
    content.style.marginTop = CONFIG.isCollapsed ? '0' : '10px';

    // 标题栏间距调整
    header.style.marginBottom = CONFIG.isCollapsed ? '0' : '10px';

    // 更新按钮图标
    header.querySelector('button').textContent = CONFIG.isCollapsed ? '▶' : '▼';
    GM_setValue('isCollapsed', CONFIG.isCollapsed);
  }

  function toggleTheme() {
    CONFIG.theme = CONFIG.theme === 'dark' ? 'light' : 'dark';
    const themeBtn = header.querySelectorAll('button')[1];
    themeBtn.textContent = CONFIG.theme === 'dark' ? '🌞' : '🌙';
    applyTheme();
    GM_setValue('theme', CONFIG.theme);
  }

  function startDrag(e) {
    isDragging = true;
    startX = e.clientX;
    startY = e.clientY;
    initLeft = parseFloat(controls.style.left);
    initTop = parseFloat(controls.style.top);
    controls.style.cursor = 'grabbing';
  }

  function handleDrag(e) {
    if (!isDragging) return;
    const dx = e.clientX - startX;
    const dy = e.clientY - startY;
    controls.style.left = `${initLeft + dx}px`;
    controls.style.top = `${initTop + dy}px`;
  }

  function endDrag() {
    if (!isDragging) return;
    isDragging = false;
    GM_setValue('controlPos', {
      x: parseFloat(controls.style.left),
      y: parseFloat(controls.style.top),
    });
  }

  function handleNumberInput(e) {
    const val = Math.min(16, Math.max(0.1, e.target.value));
    syncInputs(val);
  }

  function handleKeyboardShortcuts(e) {
    if (e.altKey) {
      const current = parseFloat(speedSlider.value);
      if (current - 0.05 < 0.1 || current + 0.05 > 16) return;
      if (e.key === 'ArrowUp') syncInputs(current + 0.05);
      if (e.key === 'ArrowDown') syncInputs(current - 0.05);
      if (e.key === 'r') syncInputs(1.0);
    }
  }

  function updateVideoElement() {
    video = document.querySelector('video');
  }

  function syncInputs(value) {
    const speed = parseFloat(value).toFixed(2);
    speedSlider.value = speed;
    numInput.value = speed;
    speedDisplay.textContent = `当前速度:${speed}x`;
    if (video) video.playbackRate = speed;
  }
})();