Greasy Fork is available in English.

视频倍速快捷键

speed up/down video

// ==UserScript==
// @name         视频倍速快捷键
// @version      1.1
// @description  speed up/down video
// @author       BlueSky
// @match        *://*.bilibili.com/*
// @match        *://*.netflix.com/*
// @match        *://*.youtube.com/*
// @grant        none
// @namespace https://greasyfork.org/users/447360
// ==/UserScript==

/**
 * configMap: {
 *   [siteName: string]: {
 *     titleSelectors: string[]     // 视频标题元素选择器
 *     videoSelector: string        // 视频元素选择器
 *     fullscreenSelector: string   // 全屏按钮元素选择器
 *     keyMap: {                    // 按键功能映射
 *       forward0_25?: string       // 速度增加 0.25x
 *       back0_25?: string          // 速度减少 0.25x
 *       forward0_5?: string        // 速度增加 0.5x
 *       back0_5?: string           // 速度减少 0.5x
 *       rate0_5?: string           // 设为 0.5x
 *       rate1?: string             // 设为 1x
 *       rate2?: string             // 设为 2x
 *       rate3?: string             // 设为 3x
 *       rate4?: string             // 设为 4x
 *       fullscreen?: string        // 切换全屏
 *     }
 *   }
 * }
 */

(function () {
  'use strict';
  const configMap = {
    bilibili: {
      titleSelectors: [
        'div.bilibili-player-video-top-title',
        'span.tit',
      ],
      videoSelector: 'bwp-video',
      fullscreenSelector: '.bilibili-player-video-btn.bilibili-player-video-btn-fullscreen',
      keyMap: {
        forward0_25: ']',
        back0_25: '[',
        forward0_5: '=',
        back0_5: '-',
        rate0_5: '`',
        rate1: '1',
        rate2: '2',
        rate3: '3',
        rate4: '4',
        fullscreen: 'Enter',
      },
    },
    youtube: {
      titleSelectors: [
        '.ytp-title-link.yt-uix-sessionlink.ytp-title-fullerscreen-link',
        'h1 > yt-formatted-string.style-scope.ytd-video-primary-info-renderer'
      ],
      videoSelector: 'video',
      fullscreenSelector: '.ytp-fullscreen-button.ytp-button',
      keyMap: {
        forward0_25: ']',
        back0_25: '[',
        forward0_5: '=',
        back0_5: '-',
        rate0_5: '`',
        fullscreen: 'Enter',
      },
    },
    netflix: {
      titleSelectors: [
        '.watch-video h4',
      ],
      videoSelector: 'video',
      fullscreenSelector: 'button[data-uia^=control-fullscreen]',
      keyMap: {
        forward0_25: ']',
        back0_25: '[',
        forward0_5: '=',
        back0_5: '-',
        rate0_5: '`',
        rate1: '1',
        rate2: '2',
        rate3: '3',
        rate4: '4',
      },
    },
  }
  let rate = 1
  let titleSelectors = []
  let videoSelector = 'video'
  let fullscreenSelector = ''
  let keyMap = {}
  const siteName = location.hostname.split('.').slice(-2)[0]
  const config = configMap[siteName]
  if (config) {
    console.debug(`found config for ${siteName}:`, config)
    titleSelectors = config.titleSelectors
    videoSelector = config.videoSelector
    fullscreenSelector = config.fullscreenSelector
    keyMap = config.keyMap
  }


  window.addEventListener('keydown', (e) => {
    if (e.key === keyMap.rate0_5) {
      rate = 0.5
    } else if (e.key === keyMap.rate1) {
      rate = 1
    } else if (e.key === keyMap.rate2) {
      rate = 2
    } else if (e.key === keyMap.rate3) {
      rate = 3
    } else if (e.key === keyMap.rate4) {
      rate = 4
    } else if (e.key === keyMap.forward0_25 && rate < 16) {
      rate += 0.25
    } else if (e.key === keyMap.back0_25 && rate > 0.25) {
      rate -= 0.25
    } else if (e.key === keyMap.forward0_5 && rate < 16) {
      rate += 0.5
    } else if (e.key === keyMap.back0_5 && rate > 0.5) {
      rate -= 0.5
    } else if (e.key === keyMap.fullscreen) {
      toggleFullScreen()
    } else {
      return
    }
    setVideoRate()
    setVideoTitle()
  })

  function getTitle() {
    return titleSelectors
      .map(selector => document.querySelector(selector))
      .filter(el => el && el.innerText)
      .map(el => el.innerText)[0]
  }

  function setVideoRate() {
    let el = document.querySelector(videoSelector) || document.querySelector('video')
    if (el) {
      console.debug(`rate: ${rate}x`)
      el.playbackRate = rate
    }
  }

  function setVideoTitle() {
    const title = (getTitle() || '').replace(/^\[.*\] /, '')
    for (const selector of titleSelectors) {
      const el = document.querySelector(selector)
      if (el) {
        el.innerHTML = `[${rate}x] ${title}`
      }
    }
  }

  // 浏览器原生全屏事件,无控制条&弹幕
  function nativeToggleFullScreen() {
    const el = document.querySelector(videoSelector)
    if (el) {
      if (document.fullscreenElement) {
        console.debug(`enter fullscreen`)
        document.exitFullscreen()
      } else {
        console.debug(`exit fullscreen`)
        el.webkitRequestFullScreen()
      }
    }
  }

  function toggleFullScreen() {
    const el = document.querySelector(fullscreenSelector)
    if (el) {
      console.debug(`toggleFullScreen`)
      el.click()
    }
  }
})();