Video Speed Buttons

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

当前为 2017-09-12 提交的版本,查看 最新版本

  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.4
  6. // @copyright 2017, Braden Best
  7. // @run-at document-end
  8. // @author Braden Best
  9. // @grant none
  10. //
  11. // @match *://*.youtube.com/*
  12. // @match *://*.vimeo.com/*
  13. // ==/UserScript==
  14.  
  15. // To add a new site: add a @match above, and modify loader_data.container_candidates near the bottom
  16.  
  17. function video_speed_buttons(anchor, video_el){
  18. if(!anchor || !video_el)
  19. return null;
  20.  
  21. const COLOR_SELECTED = "black",
  22. COLOR_NORMAL = "grey",
  23. BUTTON_SIZE = "100%",
  24. DEFAULT_SPEED = 1.0,
  25. LABEL_TEXT = "Video Speed: ";
  26.  
  27. const BUTTON_TEMPLATES = [
  28. ["25%", 0.25],
  29. ["50%", 0.5],
  30. ["Normal", 1],
  31. ["1.5x", 1.5],
  32. ["2x", 2],
  33. ["3x", 3],
  34. ["4x", 4],
  35. ["8x", 8],
  36. ["16x", 16]
  37. ];
  38.  
  39. const buttons = {
  40. head: null,
  41. selected: null,
  42. last: null
  43. };
  44.  
  45. const keyboard_controls = [
  46. [",", "Speed Down", function(ev){
  47. if(is_comment_box(ev.target))
  48. return false;
  49.  
  50. (buttons.selected || buttons.head)
  51. .getprev()
  52. .el
  53. .dispatchEvent(new MouseEvent("click"));
  54. }],
  55. [".", "Speed Up", function(ev){
  56. if(is_comment_box(ev.target))
  57. return false;
  58.  
  59. (buttons.selected || buttons.head)
  60. .getnext()
  61. .el
  62. .dispatchEvent(new MouseEvent("click"));
  63. }],
  64. ["?", "Show Help", function(ev){
  65. var infobox;
  66.  
  67. if(is_comment_box(ev.target))
  68. return false;
  69.  
  70. (infobox = Infobox(container))
  71. .log("Keyboard Controls (click to close)<br>");
  72.  
  73. keyboard_controls.forEach(function([key, description]){
  74. infobox.log(" [.s] .s<br>"
  75. .replace(".s", key)
  76. .replace(".s", description)); });
  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. const candidate = [
  112. ".comment-simplebox-text",
  113. "textarea"
  114. ].map(c => document.querySelector(c))
  115. .find(el => el !== null);
  116.  
  117. if(candidate === null){
  118. logvsb("video_speed_buttons::is_comment_box", "no candidate for comment box. Assuming false.");
  119. return 0;
  120. }
  121.  
  122. return el === candidate;
  123. }
  124.  
  125. function Infobox(parent){
  126. var el = document.createElement("pre");
  127.  
  128. el.style.font = "1em monospace";
  129. el.style.borderTop = "1px solid #ccc";
  130. el.style.marginTop = "10px";
  131. el.style.paddingTop = "10px";
  132.  
  133. el.addEventListener("click", function(){
  134. parent.removeChild(el);
  135. });
  136.  
  137. parent.appendChild(el);
  138.  
  139. function log(msg){
  140. el.innerHTML += msg;
  141. }
  142.  
  143. return {
  144. el,
  145. log
  146. };
  147. }
  148.  
  149. function setPlaybackRate(el, rate){
  150. if(el)
  151. el.playbackRate = rate;
  152. else
  153. logvsb("video_speed_buttons::setPlaybackRate", "video element is null or undefined", 1);
  154. }
  155.  
  156. function SpeedButton(text, speed, parent){
  157. var el = document.createElement("span");
  158. var self;
  159.  
  160. el.innerHTML = text;
  161. el.style.marginRight = "10px";
  162. el.style.fontWeight = "bold";
  163. el.style.fontSize = BUTTON_SIZE;
  164. el.style.color = COLOR_NORMAL;
  165. el.style.cursor = "pointer";
  166.  
  167. el.addEventListener("click", function(){
  168. setPlaybackRate(video_el, speed);
  169. self.select();
  170. });
  171.  
  172. parent.appendChild(el);
  173.  
  174. function select(){
  175. if(buttons.last !== null)
  176. buttons.last.el.style.color = COLOR_NORMAL;
  177.  
  178. buttons.last = self;
  179. buttons.selected = self;
  180. el.style.color = COLOR_SELECTED;
  181. }
  182.  
  183. function getprev(){
  184. if(self.prev === null)
  185. return self;
  186.  
  187. return buttons.selected = self.prev;
  188. }
  189.  
  190. function getnext(){
  191. if(self.next === null)
  192. return self;
  193.  
  194. return buttons.selected = self.next;
  195. }
  196.  
  197. return self = {
  198. el,
  199. text,
  200. speed,
  201. prev: null,
  202. next: null,
  203. select,
  204. getprev,
  205. getnext
  206. };
  207. }
  208.  
  209. function kill(){
  210. anchor.removeChild(container);
  211. document.body.removeEventListener("keydown", ev_keyboard);
  212. }
  213.  
  214. function ev_keyboard(ev){
  215. let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
  216. let callback = (match || {2: ()=>null})[2];
  217.  
  218. callback(ev);
  219. }
  220.  
  221. setPlaybackRate(video_el, DEFAULT_SPEED);
  222. anchor.insertBefore(container, anchor.firstChild);
  223. document.body.addEventListener("keydown", ev_keyboard);
  224.  
  225. return {
  226. controls: keyboard_controls,
  227. buttons,
  228. kill,
  229. SpeedButton,
  230. Infobox,
  231. setPlaybackRate,
  232. is_comment_box
  233. };
  234. }
  235.  
  236. video_speed_buttons.from_query = function(anchor_q, video_q){
  237. return video_speed_buttons(
  238. document.querySelector(anchor_q),
  239. document.querySelector(video_q));
  240. }
  241.  
  242. // Multi-purpose Loader (defaults to floating on top right)
  243. const loader_data = {
  244. container_candidates: [
  245. // YouTube
  246. "div#container.ytd-video-primary-info-renderer",
  247. "div#watch-header",
  248. "div#watch7-headline",
  249. "div#watch-headline-title",
  250. // Vimeo
  251. ".clip_info-wrapper",
  252. ],
  253.  
  254. css_div: [
  255. "position: fixed",
  256. "top: 0",
  257. "right: 0",
  258. "zIndex: 100",
  259. "background: rgba(0, 0, 0, 0.8)",
  260. "color: #eeeeee",
  261. "padding: 10px"
  262. ].map(rule => rule.split(/: */)),
  263.  
  264. css_vsb_container: [
  265. "borderBottom: none",
  266. "marginBottom: 0",
  267. "paddingBottom: 0",
  268. ].map(rule => rule.split(/: */))
  269. };
  270.  
  271. function logvsb(where, msg, lvl = 0){
  272. let fmt = "[vsb::$where] $msg"
  273. .replace("$where", where)
  274. .replace("$msg", msg);
  275. let logf = (["info", "error"])[lvl];
  276.  
  277. console[logf](fmt);
  278. }
  279.  
  280. function loader_loop(){
  281. let vsbc = () => document.querySelector(".vsb-container");
  282. let candidate;
  283. let default_candidate;
  284.  
  285. if(vsbc() !== null)
  286. return;
  287.  
  288. candidate = loader_data
  289. .container_candidates
  290. .map(candidate => document.querySelector(candidate))
  291. .find(candidate => candidate !== null);
  292.  
  293. default_candidate = (function(){
  294. let el = document.createElement("div");
  295.  
  296. loader_data.css_div.forEach(function([name, value]){
  297. el.style[name] = value; });
  298.  
  299. document.body.appendChild(el);
  300. return el;
  301. }());
  302.  
  303. video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
  304.  
  305. if(candidate === null){
  306. logvsb("loader_loop", "no candidates for title section. Defaulting to top of page.");
  307.  
  308. loader_data.css_vsb_container.forEach(function([name, value]){
  309. vsbc().style[name] = value;
  310. });
  311. }
  312. }
  313.  
  314. setInterval(function(){
  315. if(document.readyState === "complete")
  316. setTimeout(loader_loop, 1000);
  317. }, 1000); // Blame YouTube for this