Video Speed Buttons

Add speed buttons to any HTML5 <video> element. Comes with a loader for YouTube and Vimeo

目前为 2017-06-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Video Speed Buttons
  3. // @description Add speed buttons to any HTML5 <video> element. Comes with a loader for YouTube and Vimeo
  4. // @namespace bradenscode
  5. // @version 1.0.0
  6. // @copyright 2017, Braden Best
  7. // @run-at document-end
  8. // @grant none
  9. //
  10. // @match *://*.youtube.com/*
  11. // @match *://*.vimeo.com/*
  12. // ==/UserScript==
  13.  
  14. // To add a new site: add a @match above, and modify loader_data.container_candidates near the bottom
  15.  
  16. function video_speed_buttons(anchor, video_el){
  17. if(!anchor || !video_el)
  18. return null;
  19.  
  20. const COLOR_SELECTED = "black",
  21. COLOR_NORMAL = "grey",
  22. BUTTON_SIZE = "100%",
  23. DEFAULT_SPEED = 1.0,
  24. LABEL_TEXT = "Video Speed: ";
  25.  
  26. const BUTTON_TEMPLATES = [
  27. ["25%", 0.25],
  28. ["50%", 0.5],
  29. ["Normal", 1],
  30. ["1.5x", 1.5],
  31. ["2x", 2],
  32. ["3x", 3],
  33. ["4x", 4],
  34. ["8x", 8],
  35. ["16x", 16]
  36. ];
  37.  
  38. const buttons = {
  39. head: null,
  40. selected: null,
  41. last: null
  42. };
  43.  
  44. const keyboard_controls = [
  45. [",", "Speed Down", function(ev){
  46. if(is_comment_box(ev.target))
  47. return false;
  48.  
  49. (buttons.selected || buttons.head)
  50. .getprev()
  51. .el
  52. .dispatchEvent(new MouseEvent("click"));
  53. }],
  54. [".", "Speed Up", function(ev){
  55. if(is_comment_box(ev.target))
  56. return false;
  57.  
  58. (buttons.selected || buttons.head)
  59. .getnext()
  60. .el
  61. .dispatchEvent(new MouseEvent("click"));
  62. }],
  63. ["?", "Show Help", function(ev){
  64. var infobox;
  65.  
  66. if(is_comment_box(ev.target))
  67. return false;
  68.  
  69. (infobox = Infobox(container))
  70. .log("Keyboard Controls (click to close)<br>");
  71.  
  72. keyboard_controls.forEach(function([key, description]){
  73. infobox.log(" [.s] .s<br>"
  74. .replace(".s", key)
  75. .replace(".s", description));
  76. });
  77. }]
  78. ];
  79.  
  80. const container = (function(){
  81. var div = document.createElement("div");
  82. var prev_node = null;
  83.  
  84. div.className = "vsb-container";
  85. div.style.borderBottom = "1px solid #ccc";
  86. div.style.marginBottom = "10px";
  87. div.style.paddingBottom = "10px";
  88. div.appendChild(document.createTextNode(LABEL_TEXT));
  89.  
  90. BUTTON_TEMPLATES.forEach(function(button){
  91. var speedButton = SpeedButton(...button, div);
  92.  
  93. if(buttons.head === null)
  94. buttons.head = speedButton;
  95.  
  96. if(prev_node !== null){
  97. speedButton.prev = prev_node;
  98. prev_node.next = speedButton;
  99. }
  100.  
  101. prev_node = speedButton;
  102.  
  103. if(speedButton.speed == DEFAULT_SPEED)
  104. speedButton.select();
  105. });
  106.  
  107. return div;
  108. })();
  109.  
  110. function is_comment_box(el){
  111. return el === document.querySelector(".comment-simplebox-text");
  112. }
  113.  
  114. function Infobox(parent){
  115. var el = document.createElement("pre");
  116.  
  117. el.style.font = "1em monospace";
  118. el.style.borderTop = "1px solid #ccc";
  119. el.style.marginTop = "10px";
  120. el.style.paddingTop = "10px";
  121.  
  122. el.addEventListener("click", function(){
  123. parent.removeChild(el);
  124. });
  125.  
  126. parent.appendChild(el);
  127.  
  128. function log(msg){
  129. el.innerHTML += msg;
  130. }
  131.  
  132. return {
  133. el,
  134. log
  135. };
  136. }
  137.  
  138. function setPlaybackRate(el, rate){
  139. if(el)
  140. el.playbackRate = rate;
  141. else
  142. console.log("setPlaybackRate: video element is null or undefined");
  143. }
  144.  
  145. function SpeedButton(text, speed, parent){
  146. var el = document.createElement("span");
  147. var self;
  148.  
  149. el.innerHTML = text;
  150. el.style.marginRight = "10px";
  151. el.style.fontWeight = "bold";
  152. el.style.fontSize = BUTTON_SIZE;
  153. el.style.color = COLOR_NORMAL;
  154. el.style.cursor = "pointer";
  155.  
  156. el.addEventListener("click", function(){
  157. setPlaybackRate(video_el, speed);
  158. self.select();
  159. });
  160.  
  161. parent.appendChild(el);
  162.  
  163. function select(){
  164. if(buttons.last !== null)
  165. buttons.last.el.style.color = COLOR_NORMAL;
  166.  
  167. buttons.last = self;
  168. buttons.selected = self;
  169. el.style.color = COLOR_SELECTED;
  170. }
  171.  
  172. function getprev(){
  173. if(self.prev === null)
  174. return self;
  175.  
  176. return buttons.selected = self.prev;
  177. }
  178.  
  179. function getnext(){
  180. if(self.next === null)
  181. return self;
  182.  
  183. return buttons.selected = self.next;
  184. }
  185.  
  186. return self = {
  187. el,
  188. text,
  189. speed,
  190. prev: null,
  191. next: null,
  192. select,
  193. getprev,
  194. getnext
  195. };
  196. }
  197.  
  198. function kill(){
  199. anchor.removeChild(container);
  200. document.body.removeEventListener("keydown", ev_keyboard);
  201. }
  202.  
  203. function ev_keyboard(ev){
  204. let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
  205. let callback = (match || {2: ()=>null})[2];
  206.  
  207. callback(ev);
  208. }
  209.  
  210. setPlaybackRate(video_el, DEFAULT_SPEED);
  211. anchor.insertBefore(container, anchor.firstChild);
  212. document.body.addEventListener("keydown", ev_keyboard);
  213.  
  214. return {
  215. controls: keyboard_controls,
  216. buttons,
  217. kill,
  218. SpeedButton,
  219. Infobox
  220. };
  221. }
  222.  
  223. video_speed_buttons.from_query = function(anchor_q, video_q){
  224. return video_speed_buttons(
  225. document.querySelector(anchor_q),
  226. document.querySelector(video_q));
  227. }
  228.  
  229. // Multi-purpose Loader (defaults to floating on top right)
  230. const loader_data = {
  231. container_candidates: [
  232. "div#watch-header", // youtube (watch)
  233. ".clip_info-wrapper", // vimeo
  234. ],
  235.  
  236. css_div: [
  237. "position: fixed",
  238. "top: 0",
  239. "right: 0",
  240. "zIndex: 100",
  241. "background: rgba(0, 0, 0, 0.8)",
  242. "color: #eeeeee",
  243. "padding: 10px"
  244. ].map(rule => rule.split(/: */)),
  245.  
  246. css_vsb_container: [
  247. "borderBottom: none",
  248. "marginBottom: 0",
  249. "paddingBottom: 0",
  250. ].map(rule => rule.split(/: */))
  251. };
  252.  
  253. setInterval(function(){
  254. if(document.querySelector(".vsb-container") === null){
  255. let candidate = loader_data
  256. .container_candidates
  257. .map(candidate => document.querySelector(candidate))
  258. .find(candidate => candidate !== null);
  259.  
  260. let default_candidate = (function(){
  261. let el = document.createElement("div");
  262.  
  263. loader_data.css_div.forEach(function([name, value]){
  264. el.style[name] = value;
  265. });
  266.  
  267. document.body.appendChild(el);
  268. return el;
  269. }());
  270.  
  271. video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
  272.  
  273. if(!candidate)
  274. loader_data.css_vsb_container.forEach(function([name, value]){
  275. document.querySelector(".vsb-container").style[name] = value;
  276. });
  277. }
  278. }, 100);