// ==UserScript==
// @name video, faster
// @namespace Violentmonkey Scripts
// @include *://*/*
// @grant none
// @version 3.3
// @author KraXen72
// @description speed up video on any site
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
// TODO
// highlight currently selected playbackRate
// per-site speed & remember toggles, with a default remember toggle? could be useful
// NOTE: to get compact video speed buttons, add this css in violentMonkey custom css in settings
/*
[data-message="video, faster"] + .submenu-buttons + .submenu-commands {
display: flex;
justify-content: center;
}
[data-message="video, faster"] + .submenu-buttons + .submenu-commands .menu-item {
padding: 0.5rem;
margin: 0px;
width: auto;
}
[data-message="video, faster"] + .submenu-buttons + .submenu-commands .menu-item .icon {
display: none;
}
*/
GM_addStyle(`
.userscript-video-top-bar {
box-sizing: border-box;
background-color: black;
color: white;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 22px;
z-index: 100;
display: flex;
padding: 0;
transition: opacity 0.2s ease-in-out;
display: grid;
grid-template-columns: max-content max-content 1fr;
grid-template-rows: min-content;
}
.userscript-video-top-bar::-webkit-scrollbar { display: none }
.userscript-video-top-bar:hover { opacity: 1 }
.userscript-video-top-bar button,
.userscript-simple-btn {
margin: 0 3px;
height: 100%;
padding: 2px;
font-size: 14px;
line-height: 14px;
width: min-content;
}
.userscript-bar-wrap {
display: flex;
height: 22px;
padding: 2px 16px;
box-sizing: border-box;
}
.userscript-simple-btn {
background: #262626 !important;
border: 1px solid #191919 !important;
border-radius: 2px;
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
font-weight: normal;
}
.userscript-simple-btn:hover {
background: #303030 !important;
border-color: #383838 !important;
}
.userscript-simple-btn:active {
background: #383838 !important;
}
.userscript-simple-btn,
.userscript-simple-btn:hover,
.userscript-simple-btn:active {
color: white !important;
background-image: none !important;
box-sizing: border-box !important;
box-shadow: none !important;
text-shadow: none !important;
}
.userscript-bar-separator {
border-left: 2px solid #616161e5;
width: 0px;
margin: 0 6px;
}
.userscript-cb-wrap {
display: flex;
white-space: nowrap;
padding: 0 3px;
align-items: center;
}
.userscript-cb-wrap,
.userscript-cb-wrap > * {
width: max-content;
margin-top: 0;
margin-bottom: 0;
}
.userscript-cb-wrap > *:not(::last-child) {
margin-right: 3px;
}
.userscript-hoverinv {
opacity: 0;
}
`);
const jumpVal = 5
let videoElem = null
const commands = {
"1x": () => playbackRate(1),
"1.5x": () => playbackRate(1.5),
"2x": () => playbackRate(2),
"2.5x": () => playbackRate(2.5),
"2.75x": () => playbackRate(2.75),
"3x": () => playbackRate(3),
"3.5x": () => playbackRate(3.5),
"4x": () => playbackRate(4)
}
function findVideoElement(debug = false) {
if (!document.body) return;
if (document.body.contains(videoElem)) return;
let qs = "video";
videoElem = document.querySelector(qs);
if (videoElem !== null) {
if (debug) console.log("found video elem", videoElem, qs);
} else {
// Look for video elements within iframes
const iframes = document.querySelectorAll("iframe");
iframes.forEach(iframe => {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const iframeVideoElem = iframeDoc.querySelector("video");
if (iframeVideoElem) {
videoElem = iframeVideoElem;
if (debug) console.log("found video elem in iframe", videoElem, iframe.src);
}
});
}
}
function playbackRate(rate, e = null, video = null) {
if (video == null) {
findVideoElement()
video = videoElem
}
if (e != null) cancelEvent(e);
// let wasplaying = !video.paused
// if (wasplaying) video.pause()
video.playbackRate = rate
// if (wasplaying && video.paused) video.play()
GM_setValue("lastSpeed", rate)
}
function registerCommands() {
Object.keys(commands).forEach(command => {
try {
GM_unregisterMenuCommand(command)
} catch (e) { console.error(e) }
})
Object.entries(commands).forEach(command => {
GM_registerMenuCommand(command[0], command[1])
})
}
registerCommands()
findVideoElement()
// new part of script
function cancelEvent(e) {
if (!e || e == null) return;
e.preventDefault()
e.stopPropagation()
}
function ff(vid = null, e) {
cancelEvent(e);
if (!vid) vid = videoElem;
vid.currentTime += jumpVal;
}
function rw(vid = null, e) {
cancelEvent(e);
if (!vid) vid = videoElem;
vid.currentTime -= jumpVal;
}
function pp(vid = null, e) {
cancelEvent(e);
if (vid.paused) {
vid.play()
} else {
vid.pause()
}
}
const btnDefaults = { classList: "userscript-simple-btn" }
let seekHotkeysEnabled = false
function checkBox(emoji, valueKey = "", title = "", defaultv = false) {
const wrap = document.createElement("span")
wrap.classList.add("userscript-simple-btn", "userscript-cb-wrap")
wrap.textContent = emoji
wrap.onclick = cancelEvent
if (title) wrap.title = title
const cb = Object.assign(document.createElement("input"), { type: "checkbox" })
cb.style.marginLeft = "7px"
cb.checked = valueKey ? GM_getValue(valueKey, defaultv) : false
cb.onclick = (e) => {
if (!valueKey) return;
e.stopPropagation();
GM_setValue(valueKey, e.target.checked)
}
wrap.appendChild(cb)
return wrap
}
function addVideoTopBar(video) {
if (video.previousElementSibling !== null && video.previousElementSibling.classList.contains("userscript-video-top-bar")) return;
const topBar = document.createElement('div');
topBar.classList.add('userscript-video-top-bar')
const shouldBePinned = GM_getValue("pinned", false)
if (!shouldBePinned) topBar.classList.add("userscript-hoverinv");
const leftdiv = Object.assign(document.createElement("div"), { classList: "userscript-bar-wrap" })
const centerdiv = Object.assign(document.createElement("div"), { classList: "userscript-bar-wrap" })
const rightdiv = Object.assign(document.createElement("div"), { classList: "userscript-bar-wrap", style: "justify-content: end" })
for (const cmd of Object.keys(commands)) {
const btn = document.createElement("button")
btn.textContent = cmd
btn.classList = "userscript-simple-btn"
btn.onclick = (e) => playbackRate(Number(cmd.replace("x", "")), e, video)
leftdiv.appendChild(btn)
}
centerdiv.appendChild(Object.assign(document.createElement("button"), {...btnDefaults, textContent: "<<", onclick: (e) => rw(video, e), title:`rewind ${jumpVal}s` }))
centerdiv.appendChild(Object.assign(document.createElement("button"), {...btnDefaults, textContent: "⏯", onclick: (e) => pp(video, e), title:`play/pause` }))
centerdiv.appendChild(Object.assign(document.createElement("button"), {...btnDefaults, textContent: ">>", onclick: (e) => ff(video, e), title:`forward ${jumpVal}s` }))
rightdiv.appendChild(checkBox("💾🐇", "rememberSpeed", "remember video speed across sites."))
// const hotkeyCb = checkBox("⏪⏩", "add left/right 5s seek hotkeys (this site)")
// hotkeyCb.onclick = e => {e.stopPropagation(); console.log(e.target.checked)}
// rightdiv.appendChild(hotkeyCb)
rightdiv.appendChild(Object.assign(document.createElement("button"), {
...btnDefaults,
onclick: function(e) {
cancelEvent(e);
topBar.classList.toggle("userscript-hoverinv");
const isPinned = !topBar.classList.contains("userscript-hoverinv")
this.textContent = isPinned ? "📍" : "📌"
this.title = isPinned ? "unpin" : "pin"
GM_setValue("pinned", isPinned)
},
textContent: shouldBePinned ? "📍": "📌",
title: shouldBePinned ? "unpin" : "pin",
style: "postition: relative;"
}))
topBar.appendChild(leftdiv)
topBar.appendChild(centerdiv)
topBar.appendChild(rightdiv)
if (video.parentElement.querySelectorAll("video").length > 1) {
topBar.style.position = "relative"
topBar.style.overflowX = "auto"
video.addEventListener("resize", () => {
topBar.style.width = `${video.clientWidth}px`;
})
topBar.querySelectorAll(".userscript-bar-wrap").forEach(wrapper => { Object.assign(wrapper.style, { paddingLeft: 0, paddingRight: 0 })})
topBar.style.width = `${video.clientWidth}px`;
video.parentElement.insertBefore(topBar, video);
} else {
video.parentElement.insertBefore(topBar, video);
}
const lastSpeed = GM_getValue("lastSpeed", 0)
const restoreSpeed = () => playbackRate(Number(lastSpeed), null, video);
if (GM_getValue("rememberSpeed", false) && lastSpeed) {
if (!!video.paused) {
video.addEventListener("play", () => {
if (GM_getValue("rememberSpeed", false) === false) return;
setTimeout(restoreSpeed, 1) // hopefully run after any external code
setTimeout(() => { if (video.playbackRate !== Number(lastSpeed)) restoreSpeed() }, 125) // MAKE SURE
}, { once: true })
} else {
restoreSpeed()
}
}
}
let observer = null;
if (observer === null) {
observer = new MutationObserver(MOCallback)
observer.observe(document.body, { childList: true, subtree: true });
} else {
console.log("observer is already defined:", observer, "restarting it...")
observer.disconnect() // script has re-ran for whatever reason, but the observer is defined. restart observer to be safe
observer.observe(document.body, { childList: true, subtree: true })
}
function MOCallback(mutationsList, observer) {
for(const mutation of mutationsList) {
if (mutation.type !== 'childList') continue;
mutation.addedNodes.forEach(node => {
if (node.tagName && node.tagName.toLowerCase() === 'video') {
console.log('A video element has been added to the document:', node);
addVideoTopBar(node)
}
});
}
};
document.querySelectorAll("video").forEach(addVideoTopBar)