// ==UserScript==
// @name Video Speed Buttons
// @description Add speed buttons to any HTML5 <video> element. Comes with a loader for YouTube and Vimeo
// @namespace bradenscode
// @version 1.0.10
// @copyright 2017, Braden Best
// @run-at document-end
// @author Braden Best
// @grant none
//
// @match *://*.youtube.com/*
// @match *://youtube.com/*
// @match *://*.vimeo.com/*
// @match *://vimeo.com/*
// ==/UserScript==
// To add a new site: add a @match above, and modify loader_data.container_candidates near the bottom
const CONTROLLER_VSB = 0; // normal controller (video speed buttons). Uses lots of loops to enforce speed
const CONTROLLER_VSC = 1; // alternative controller (video speed controller). Keyboard-only, minimalistic, no loops
const controller_type = CONTROLLER_VSB;
// change this to use the experimental CONTROLLER_VSC, which is keyboard-only
// and extremely minimalistic, but is also fast, lightweight on memory usage,
// and doesn't use any loops. Try it out, see if it works better for you.
// The controls are the same as VSB. + to increase the speed, - to decrease,
// * to reset to 1.
function video_speed_buttons(anchor, video_el){
if(!anchor || !video_el)
return null;
const COLOR_SELECTED = "#FF5500",
COLOR_NORMAL = "grey",
BUTTON_SIZE = "120%",
DEFAULT_SPEED = 1.0,
LABEL_TEXT = "Video Speed: ",
ALLOW_EXTERNAL_ACCESS = false;
const BUTTON_TEMPLATES = [
["25%", 0.25],
["50%", 0.5],
["Normal", 1],
["1.5x", 1.5],
["2x", 2],
["3x", 3],
["4x", 4],
["8x", 8],
["16x", 16]
];
const buttons = {
head: null,
selected: null,
last: null
};
const keyboard_controls = [
["-", "Speed Down", function(ev){
if(is_comment_box(ev.target))
return false;
(buttons.selected || buttons.head)
.getprev()
.el
.dispatchEvent(new MouseEvent("click"));
}],
["+", "Speed Up", function(ev){
if(is_comment_box(ev.target))
return false;
(buttons.selected || buttons.head)
.getnext()
.el
.dispatchEvent(new MouseEvent("click"));
}],
["*", "Reset Speed", function(ev){
let selbtn = buttons.head;
let result = null;
if(is_comment_box(ev.target))
return false;
while(selbtn !== null && result === null)
if(selbtn.speed === DEFAULT_SPEED)
result = selbtn;
else
selbtn = selbtn.next;
if(result === null)
result = buttons.head;
result.el.dispatchEvent(new MouseEvent("click"));
}],
["?", "Show Help", function(ev){
let infobox;
if(is_comment_box(ev.target))
return false;
(infobox = Infobox(container))
.log("Keyboard Controls (click to close)<br>");
keyboard_controls.forEach(function([key, description]){
infobox.log(` [${key}] ${description}<br>`);
});
}]
];
const container = (function(){
let div = document.createElement("div");
let prev_node = null;
div.className = "vsb-container";
div.style.borderBottom = "1px solid #ccc";
div.style.marginBottom = "10px";
div.style.paddingBottom = "10px";
div.appendChild(SpeedButtonLabel(LABEL_TEXT));
BUTTON_TEMPLATES.forEach(function(button){
let speedButton = SpeedButton(...button, div);
if(buttons.head === null)
buttons.head = speedButton;
if(prev_node !== null){
speedButton.prev = prev_node;
prev_node.next = speedButton;
}
prev_node = speedButton;
if(speedButton.speed == DEFAULT_SPEED)
speedButton.select();
});
return div;
})();
function is_comment_box(el){
const candidate = [
".comment-simplebox-text",
"textarea"
].map(c => document.querySelector(c))
.find(el => el !== null);
if(candidate === null){
logvsb("video_speed_buttons::is_comment_box", "no candidate for comment box. Assuming false.");
return 0;
}
return el === candidate;
}
function Infobox(parent){
let el = document.createElement("pre");
el.style.font = "1em monospace";
el.style.borderTop = "1px solid #ccc";
el.style.marginTop = "10px";
el.style.paddingTop = "10px";
el.addEventListener("click", function(){
parent.removeChild(el);
});
parent.appendChild(el);
function log(msg){
el.innerHTML += msg;
}
return {
el,
log
};
}
let playbackRate_data = {
rate: 1,
video: null,
};
function setPlaybackRate(el, rate){
if(el) {
el.playbackRate = rate;
playbackRate_data.rate = rate;
playbackRate_data.video = el;
}
else
logvsb("video_speed_buttons::setPlaybackRate", "video element is null or undefined", 1);
}
function SpeedButtonLabel(text){
let el = document.createElement("span");
el.innerHTML = text;
el.style.marginRight = "10px";
el.style.fontWeight = "bold";
el.style.fontSize = BUTTON_SIZE;
el.style.color = COLOR_NORMAL;
return el;
}
function SpeedButton(text, speed, parent){
let el = SpeedButtonLabel(text);
let self;
el.style.cursor = "pointer";
el.addEventListener("click", function(){
setPlaybackRate(video_el, speed);
self.select();
});
parent.appendChild(el);
function select(){
if(buttons.last !== null)
buttons.last.el.style.color = COLOR_NORMAL;
buttons.last = self;
buttons.selected = self;
el.style.color = COLOR_SELECTED;
}
function getprev(){
if(self.prev === null)
return self;
return buttons.selected = self.prev;
}
function getnext(){
if(self.next === null)
return self;
return buttons.selected = self.next;
}
return self = {
el,
text,
speed,
prev: null,
next: null,
select,
getprev,
getnext
};
}
function kill(){
anchor.removeChild(container);
document.body.removeEventListener("keydown", ev_keyboard);
}
function set_video_el(new_video_el){
video_el = new_video_el;
}
function ev_keyboard(ev){
let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
let callback = (match || {2: ()=>null})[2];
callback(ev);
}
setPlaybackRate(video_el, DEFAULT_SPEED);
anchor.insertBefore(container, anchor.firstChild);
document.body.addEventListener("keydown", ev_keyboard);
return {
controls: keyboard_controls,
buttons,
kill,
SpeedButton,
Infobox,
setPlaybackRate,
is_comment_box,
set_video_el,
playbackRate_data,
ALLOW_EXTERNAL_ACCESS,
};
}
video_speed_buttons.from_query = function(anchor_q, video_q){
return video_speed_buttons(
document.querySelector(anchor_q),
document.querySelector(video_q));
}
// Multi-purpose Loader (defaults to floating on top right)
const loader_data = {
container_candidates: [
// YouTube
"div#above-the-fold",
"div#title.style-scope.ytd-watch-metadata",
"div#container.ytd-video-primary-info-renderer",
"div#watch-header",
"div#watch7-headline",
"div#watch-headline-title",
// Vimeo
".clip_info-wrapper",
],
css_div: [
"position: fixed",
"top: 0",
"right: 0",
"zIndex: 100000",
"background: rgba(0, 0, 0, 0.8)",
"color: #eeeeee",
"padding: 10px"
].map(rule => rule.split(/: */)),
css_vsb_container: [
"borderBottom: none",
"marginBottom: 0",
"paddingBottom: 0",
].map(rule => rule.split(/: */))
};
function logvsb(where, msg, lvl = 0){
let logf = (["info", "error"])[lvl];
console[logf](`[vsb::${where}] ${msg}`);
}
function loader_loop(){
let vsbc = () => document.querySelector(".vsb-container");
let candidate;
let default_candidate;
let vsb_handle;
if(vsbc() !== null)
return;
candidate = loader_data
.container_candidates
.map(candidate => document.querySelector(candidate))
.find(candidate => candidate !== null);
default_candidate = (function(){
let el = document.createElement("div");
loader_data.css_div.forEach(function([name, value]){
el.style[name] = value; });
return el;
}());
vsb_handle = video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
if(candidate === null){
logvsb("loader_loop", "no candidates for title section. Defaulting to top of page.");
document.body.appendChild(default_candidate);
loader_data.css_vsb_container.forEach(function([name, value]){
vsbc().style[name] = value;
});
}
// ugly hack to address vimeo automatically resetting the speed
vsb_handle.enforcer_loop_iid = setInterval(function(){
let prdata = vsb_handle.playbackRate_data;
if (prdata.video !== null)
prdata.video.playbackRate = prdata.rate;
}, 500);
if(vsb_handle.ALLOW_EXTERNAL_ACCESS)
window.vsb = vsb_handle;
}
const vsc = {
name: "Video Speed Controller",
getvideo: _ => document.querySelector("video"), // Yep, it's really that simple.
rates: [0.25, 0.5, 1, 1.5, 2, 3, 4, 8, 16],
selrate: 2,
ev_keydown: function(ev) {
let change = 0;
if (vsc.getvideo() === null)
return true;
if (ev.key === "+")
change = +1;
if (ev.key === "-")
change = -1;
if (ev.key === "*")
change = -(vsc.selrate - 2);
vsc.selrate = (vsc.rates.length + vsc.selrate + change) % vsc.rates.length;
vsc.getvideo().playbackRate = vsc.rates[vsc.selrate];
console.log(`[${vsc.name}] Speed set to ${vsc.rates[vsc.selrate]}`);
}
};
if (controller_type === CONTROLLER_VSB) {
setInterval(function(){
if(document.readyState === "complete")
setTimeout(loader_loop, 1000);
}, 1000); // Blame YouTube for this
}
else if (controller_type === CONTROLLER_VSC) {
document.body.addEventListener("keydown", vsc.ev_keydown);
console.clear();
console.log(`[${vsc.name}] loaded`);
}