- // ==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
- };
- }
-
- 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);