// ==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.0
// @copyright 2017, Braden Best
// @run-at document-end
// @grant none
//
// @match *://*.youtube.com/*
// @match *://*.vimeo.com/*
// ==/UserScript==
// To add a new site: add a @match above, and modify loader_data.container_candidates near the bottom
function video_speed_buttons(anchor, video_el){
if(!anchor || !video_el)
return null;
const COLOR_SELECTED = "black",
COLOR_NORMAL = "grey",
BUTTON_SIZE = "100%",
DEFAULT_SPEED = 1.0,
LABEL_TEXT = "Video Speed: ";
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"));
}],
["?", "Show Help", function(ev){
var 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(" [.s] .s<br>"
.replace(".s", key)
.replace(".s", description));
});
}]
];
const container = (function(){
var div = document.createElement("div");
var prev_node = null;
div.className = "vsb-container";
div.style.borderBottom = "1px solid #ccc";
div.style.marginBottom = "10px";
div.style.paddingBottom = "10px";
div.appendChild(document.createTextNode(LABEL_TEXT));
BUTTON_TEMPLATES.forEach(function(button){
var 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){
return el === document.querySelector(".comment-simplebox-text");
}
function Infobox(parent){
var 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
};
}
function setPlaybackRate(el, rate){
if(el)
el.playbackRate = rate;
else
console.log("setPlaybackRate: video element is null or undefined");
}
function SpeedButton(text, speed, parent){
var el = document.createElement("span");
var self;
el.innerHTML = text;
el.style.marginRight = "10px";
el.style.fontWeight = "bold";
el.style.fontSize = BUTTON_SIZE;
el.style.color = COLOR_NORMAL;
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 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
};
}
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: [
"div#watch-header", // youtube (watch)
".clip_info-wrapper", // vimeo
],
css_div: [
"position: fixed",
"top: 0",
"right: 0",
"zIndex: 100",
"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(/: */))
};
setInterval(function(){
if(document.querySelector(".vsb-container") === null){
let candidate = loader_data
.container_candidates
.map(candidate => document.querySelector(candidate))
.find(candidate => candidate !== null);
let default_candidate = (function(){
let el = document.createElement("div");
loader_data.css_div.forEach(function([name, value]){
el.style[name] = value;
});
document.body.appendChild(el);
return el;
}());
video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
if(!candidate)
loader_data.css_vsb_container.forEach(function([name, value]){
document.querySelector(".vsb-container").style[name] = value;
});
}
}, 100);