您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds playback shortcuts to video players. ('Ctrl + >'/'Ctrl + <' to change playback rate, 'Ctrl + .' to enter PiP)
- // ==UserScript==
- // @name Playback Shortcuts
- // @namespace endorh
- // @version 1.1
- // @description Adds playback shortcuts to video players. ('Ctrl + >'/'Ctrl + <' to change playback rate, 'Ctrl + .' to enter PiP)
- // @author endorh
- // @match https://*/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
- // @license MIT
- // @grant GM_addStyle
- // ==/UserScript==
- (function() {
- 'use strict';
- // == UserSettings ==
- // Hotkeys
- let enterPiPHotkey = (e) => e.key == '.' && e.ctrlKey
- let decreasePlaybackRateHotkey = (e) => e.key == '<' && e.ctrlKey
- let increasePlaybackRateHotkey = (e) => e.key == '>' && e.ctrlKey
- // Playback rate steps (must be sorted!)
- let playbackRates = [
- 0.01, 0.025, 0.05, 0.1, 0.15, 0.20,
- 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2,
- 2.5, 3, 3.5, 4, 5, 7.5, 10, 15, 20
- ] // Extra playback steps
- // let playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] // Classic YouTube rate steps
- // Playback overlay fade timeout in ms
- let fadeTimeout = 350 // ms
- // CSS style of the overlay. The overlay consists of three divs:
- // - outer container (sibling to the video element)
- // It's applied the `globalPlaybackRateOverlayFadeOut` class after `fadeTimeout` ms
- // have happened since the last playback rate change
- // - container (child to the outer container)
- // Has the .globalPlaybackRateOverlayContainer class
- // Its `position` should be `absolute`, as its actual position rectangle is updated
- // on every playback rate change to match that of the video.
- // Should be entirely transparent, not interactable and have a high z-index.
- // - overlay (child to the container)
- // Its content is set to `${video.playbackRate}x` after each playback rate change.
- // Can be centered with respect to its parent, which should match the dimensions of
- // the video.
- // Should be semitransparent to not disturb the video.
- GM_addStyle(`
- /* Container style */
- .globalPlaybackRateOverlayContainer {
- position: absolute;
- transition: opacity 0.05s;
- pointer-events: none;
- z-index: 2147483647;
- }
- /* Container style after fadeTimeout */
- .globalPlaybackRateOverlayFadeOut {
- transition: opacity 0.35s;
- opacity: 0%;
- }
- /* Overlay div style */
- .globalPlaybackRateOverlay {
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- padding: 0.5em;
- font-size: 24px;
- border-radius: 0.25em;
- color: #FEFEFEFE !important;
- background-color: #000000A0 !important;
- }
- `)
- // Set to true to enable console messages
- let debug = false
- // Set to true to enable console warnings
- let warn = debug || true
- // Set to false if a website is using modification observers near the video element
- // to detect added overlays, breaking the video.
- let useOverlay = true
- // == Script ==
- // Object to preserve state across events
- let state = {};
- // Add the keyboard hook
- document.addEventListener('keydown', function(e) {
- if (enterPiPHotkey(e)) {
- if (enterPiP()) {
- if (debug) console.info("Entered PiP")
- }
- } else if (decreasePlaybackRateHotkey(e)) {
- if (modifyPlaybackRate(false)) {
- if (debug) console.info("Decreased playback rate")
- e.preventDefault()
- }
- } else if (increasePlaybackRateHotkey(e)) {
- if (modifyPlaybackRate(true)) {
- if (debug) console.info("Increased playback rate")
- e.preventDefault()
- }
- }
- })
- function findVideoElement() {
- // Find video element
- let videos = [...document.getElementsByTagName('video')]
- var video = null
- if (videos.length == 0) {
- if (debug) console.info("No video found.")
- return null
- } else if (videos.length == 1) {
- video = videos[0]
- } else {
- if (warn) console.warn("Multiple videos found, using only the first video found")
- if (debug) console.log(videos);
- video = videos[0]
- }
- return video
- }
- // Enter PiP (Picture-in-Picture)
- function enterPiP() {
- let video = findVideoElement()
- if (video == null) return false
- let doc = video.ownerDocument
- // Toggle Picture-in-Picture
- if (doc.pictureInPictureElement != video) {
- if (doc.pictureInPictureElement) {
- doc.exitPictureInPicture()
- }
- video.requestPictureInPicture()
- } else doc.exitPictureInPicture()
- return true
- }
- // Modify the playback
- function modifyPlaybackRate(faster) {
- let video = findVideoElement()
- if (video == null) return false
- // Current playback rate
- let pr = video.playbackRate
- // Find target playback (comparisons use a 1e-7 delta to avoid rounding nonsense)
- let target = faster? playbackRates.find(r => r > pr + 1e-7) : playbackRates.findLast(r => r < pr - 1e-7)
- if (debug) console.info("Changing playbackRate: " + pr + " -> " + target)
- // Set playback rate
- video.playbackRate = target
- if (debug) console.log("Modified playbackRate: " + video.playbackRate)
- // Check changed playback rate
- if (warn && video.playbackRate != target) {
- console.warn("Could not modify playbackRate!\nTarget: " + target + "\nActual: " + video.playbackRate)
- }
- // Display overlay with the final playback rate
- if (useOverlay) updateOverlay(video, video.playbackRate);
- return true
- }
- // Display an overlay with the updated playback rate
- function updateOverlay(v, rate) {
- // Reuse previous overlay
- var container = null
- if (state.overlay !== undefined) {
- if (state.timeout !== undefined) clearTimeout(state.timeout)
- container = state.overlay
- } else container = document.createElement('div')
- // Inline positions and rate value
- let parent = v.parentElement
- let r = v.getBoundingClientRect()
- let p = parent.getBoundingClientRect()
- let html = `
- <div class="globalPlaybackRateOverlayContainer" style="
- left: ${r.left - p.left}px;
- right: ${r.right - p.left}px;
- top: ${r.top - p.top}px;
- bottom: ${r.bottom - p.top}px;
- width: ${r.width}px;
- height: ${r.height}px;
- ">
- <div class="globalPlaybackRateOverlay">
- ${rate}x
- </div>
- </div>
- `
- container.innerHTML = html
- // Remove fade out
- container.classList.remove("globalPlaybackRateOverlayFadeOut")
- // Add overlay
- if (state.overlay !== undefined && state.overlay.parentElement != v.parentElement) {
- state.overlay.parentElement.removeChild(state.overlay)
- state.overlay = undefined
- }
- if (state.overlay === undefined) {
- v.parentElement.appendChild(container)
- state.overlay = container
- }
- // Set timeout for the fade out animation
- state.timeout = setTimeout(function() {
- container.classList.add("globalPlaybackRateOverlayFadeOut")
- state.timeout = undefined
- }, fadeTimeout);
- }
- })();