Last active
July 4, 2024 14:07
-
-
Save schuhwerk/67fb4da50652681b857002e1ba2bf071 to your computer and use it in GitHub Desktop.
UserScript. Control Video Playback Speed with Keyboard.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Video Speed Control with Keyboard | |
// @description Decrease and increase HTML video playback speed with "," and ".". Remembers and applies speeds across page-loads. | |
// @version 2024-07-04 | |
// @author Vitus Schuhwerk | |
// @license MIT | |
// @homepageURL https://gist.githubusercontent.com/schuhwerk/67fb4da50652681b857002e1ba2bf071 | |
// @updateURL https://gist.githubusercontent.com/schuhwerk/67fb4da50652681b857002e1ba2bf071/raw | |
// @downloadURL https://gist.githubusercontent.com/schuhwerk/67fb4da50652681b857002e1ba2bf071/raw | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_addStyle | |
// @grant GM_addElement | |
// ==/UserScript== | |
const videoPlaybackControl = (() => { | |
const transformSpeedKeys = { | |
"," : ( speed ) => speed - 0.25, | |
"." : ( speed ) => speed + 0.25, | |
} | |
const isForbiddenTarget = ( target ) => [ "input", "textarea" ].includes( target?.localName ) || target?.isContentEditable | |
let videoElm; // Stores currently playing video element | |
const speedStorageKey = "playbackSpeed" | |
const speed = { | |
setApply: ( s = GM_getValue(speedStorageKey, 1 ), vElm = null ) => { | |
let vE = vElm ?? videoElm ?? getFirstVideElm() | |
s = Math.max( 0.25, Math.min( s, 10 )) | |
if ( ! vE?.playbackRate || vE?.playbackRate == s ) { | |
console.log( "No video elm found" ) | |
return | |
} | |
//console.log( "Apply speed of ", s , "to", videoElm ) | |
easyToast.success( s.toFixed(2) ) | |
GM_setValue(speedStorageKey, s ); | |
videoElm.playbackRate = s | |
}, | |
get: () => GM_getValue(speedStorageKey, 1 ) | |
} | |
const findRoots = (ele) => { | |
return [ | |
ele, | |
...ele.querySelectorAll('*') | |
].filter(e => !!e.shadowRoot) | |
.flatMap(e => [e.shadowRoot, ...findRoots(e.shadowRoot)]) | |
} | |
const getFirstVideElm = () => { | |
console.log("finding vide Elements on page") | |
let video = document.querySelector( 'video' ) | |
if ( video ) return video; | |
let shadowElems = document.body ? findRoots( document.body ) : [] | |
console.log( shadowElems ) | |
for( const elm of shadowElems ){ | |
video = elm.querySelector( 'video' ) | |
// console.log( "video for shadow", elm, video) | |
if ( video ){ | |
return video // return early on the first video found. | |
} | |
} | |
console.log( "no video found on page" ) | |
} | |
const registerShortcutKeys = ( videoElmPrm = null ) => { | |
videoElm = videoElmPrm ?? getFirstVideElm() | |
console.log("add keydown event listener", videoElm) | |
document.addEventListener("keydown", handlePressedKey); | |
speed.setApply( speed.get() ) | |
} | |
const handlePressedKey = (e) => { | |
if ( isForbiddenTarget( e.target )) return; // If the pressed key is coming from any input field, do nothing. | |
if ( ! Object.keys( transformSpeedKeys ).includes(e.key ) ) return // Not an interesting key. | |
speed.setApply( transformSpeedKeys[e.key]( speed.get() ) ) | |
} | |
return { | |
init : () => { | |
setTimeout(() => registerShortcutKeys(), 3000) // react apps, which load content later (in shadow dom). | |
document.addEventListener("playing", (e) => registerShortcutKeys( e.target ), { capture: true, once: true }); | |
document.addEventListener("playing", (e) => speed.setApply( speed.get(), e.target ) , { capture: true }); | |
document.addEventListener("play", (e) => videoElm = e.target, true); | |
document.addEventListener("DOMContentLoaded", () => setTimeout(() => registerShortcutKeys(), 3000 )); | |
} | |
} | |
})() | |
videoPlaybackControl.init() | |
/* show message to user, then hide. */ | |
const easyToast = { | |
timeoutID: null, | |
toastClass: 'ntfy-toast', | |
addStyle() { | |
GM_addStyle(`.${this.toastClass} { | |
position: fixed; color: white; background: linear-gradient(to right, rgb(7 128 113), rgb(111 155 35)); | |
z-index: 9990; padding: 10px 20px; margin: 3vw; border-radius: 5px; font-size: 18px; right: 0; top: 0; | |
}`) | |
}, | |
success(message) { | |
this.addStyle() | |
this.addStyle = () => {} // overwrite, only add on first use. | |
this.removeAndClear(); | |
GM_addElement(document.body, 'div', { class: this.toastClass, textContent: message }); | |
this.timeoutID = setTimeout(() => this.removeAndClear(), 2500); | |
}, | |
clear(){ | |
if (typeof this.timeoutID === "number") { | |
console.log("cancel!", this.timeoutID); | |
clearTimeout(this.timeoutID); | |
this.timeoutID = null; // reset the timeoutID after clearing it | |
} | |
}, | |
removeAndClear() { | |
document.querySelectorAll('.'+this.toastClass).forEach(e => e.remove()); | |
this.clear() | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment