您需要先安装一个扩展,例如 篡改猴、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); } })();