Video Speed Buttons

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

目前為 2018-05-22 提交的版本,檢視 最新版本

  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.8.1
  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. ["*", "Reset Speed", function(ev){
  65. let selbtn = buttons.head;
  66. let result = null;
  67.  
  68. if(is_comment_box(ev.target))
  69. return false;
  70.  
  71. while(selbtn !== null && result === null)
  72. if(selbtn.speed === DEFAULT_SPEED)
  73. result = selbtn;
  74. else
  75. selbtn = selbtn.next;
  76.  
  77. if(result === null)
  78. result = buttons.head;
  79.  
  80. result.el.dispatchEvent(new MouseEvent("click"));
  81. }],
  82. ["?", "Show Help", function(ev){
  83. let infobox;
  84.  
  85. if(is_comment_box(ev.target))
  86. return false;
  87.  
  88. (infobox = Infobox(container))
  89. .log("Keyboard Controls (click to close)<br>");
  90.  
  91. keyboard_controls.forEach(function([key, description]){
  92. infobox.log(` [${key}] ${description}<br>`);
  93. });
  94. }]
  95. ];
  96.  
  97. const container = (function(){
  98. let div = document.createElement("div");
  99. let prev_node = null;
  100.  
  101. div.className = "vsb-container";
  102. div.style.borderBottom = "1px solid #ccc";
  103. div.style.marginBottom = "10px";
  104. div.style.paddingBottom = "10px";
  105. div.appendChild(document.createTextNode(LABEL_TEXT));
  106.  
  107. BUTTON_TEMPLATES.forEach(function(button){
  108. let speedButton = SpeedButton(...button, div);
  109.  
  110. if(buttons.head === null)
  111. buttons.head = speedButton;
  112.  
  113. if(prev_node !== null){
  114. speedButton.prev = prev_node;
  115. prev_node.next = speedButton;
  116. }
  117.  
  118. prev_node = speedButton;
  119.  
  120. if(speedButton.speed == DEFAULT_SPEED)
  121. speedButton.select();
  122. });
  123.  
  124. return div;
  125. })();
  126.  
  127. function is_comment_box(el){
  128. const candidate = [
  129. ".comment-simplebox-text",
  130. "textarea"
  131. ].map(c => document.querySelector(c))
  132. .find(el => el !== null);
  133.  
  134. if(candidate === null){
  135. logvsb("video_speed_buttons::is_comment_box", "no candidate for comment box. Assuming false.");
  136. return 0;
  137. }
  138.  
  139. return el === candidate;
  140. }
  141.  
  142. function Infobox(parent){
  143. let el = document.createElement("pre");
  144.  
  145. el.style.font = "1em monospace";
  146. el.style.borderTop = "1px solid #ccc";
  147. el.style.marginTop = "10px";
  148. el.style.paddingTop = "10px";
  149.  
  150. el.addEventListener("click", function(){
  151. parent.removeChild(el);
  152. });
  153.  
  154. parent.appendChild(el);
  155.  
  156. function log(msg){
  157. el.innerHTML += msg;
  158. }
  159.  
  160. return {
  161. el,
  162. log
  163. };
  164. }
  165.  
  166. function setPlaybackRate(el, rate){
  167. if(el)
  168. el.playbackRate = rate;
  169. else
  170. logvsb("video_speed_buttons::setPlaybackRate", "video element is null or undefined", 1);
  171. }
  172.  
  173. function SpeedButton(text, speed, parent){
  174. let el = document.createElement("span");
  175. let self;
  176.  
  177. el.innerHTML = text;
  178. el.style.marginRight = "10px";
  179. el.style.fontWeight = "bold";
  180. el.style.fontSize = BUTTON_SIZE;
  181. el.style.color = COLOR_NORMAL;
  182. el.style.cursor = "pointer";
  183.  
  184. el.addEventListener("click", function(){
  185. setPlaybackRate(video_el, speed);
  186. self.select();
  187. });
  188.  
  189. parent.appendChild(el);
  190.  
  191. function select(){
  192. if(buttons.last !== null)
  193. buttons.last.el.style.color = COLOR_NORMAL;
  194.  
  195. buttons.last = self;
  196. buttons.selected = self;
  197. el.style.color = COLOR_SELECTED;
  198. }
  199.  
  200. function getprev(){
  201. if(self.prev === null)
  202. return self;
  203.  
  204. return buttons.selected = self.prev;
  205. }
  206.  
  207. function getnext(){
  208. if(self.next === null)
  209. return self;
  210.  
  211. return buttons.selected = self.next;
  212. }
  213.  
  214. return self = {
  215. el,
  216. text,
  217. speed,
  218. prev: null,
  219. next: null,
  220. select,
  221. getprev,
  222. getnext
  223. };
  224. }
  225.  
  226. function kill(){
  227. anchor.removeChild(container);
  228. document.body.removeEventListener("keydown", ev_keyboard);
  229. }
  230.  
  231. function ev_keyboard(ev){
  232. let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
  233. let callback = (match || {2: ()=>null})[2];
  234.  
  235. callback(ev);
  236. }
  237.  
  238. setPlaybackRate(video_el, DEFAULT_SPEED);
  239. anchor.insertBefore(container, anchor.firstChild);
  240. document.body.addEventListener("keydown", ev_keyboard);
  241.  
  242. return {
  243. controls: keyboard_controls,
  244. buttons,
  245. kill,
  246. SpeedButton,
  247. Infobox,
  248. setPlaybackRate,
  249. is_comment_box
  250. };
  251. }
  252.  
  253. video_speed_buttons.from_query = function(anchor_q, video_q){
  254. return video_speed_buttons(
  255. document.querySelector(anchor_q),
  256. document.querySelector(video_q));
  257. }
  258.  
  259. // Multi-purpose Loader (defaults to floating on top right)
  260. const loader_data = {
  261. container_candidates: [
  262. // YouTube
  263. "div#container.ytd-video-primary-info-renderer",
  264. "div#watch-header",
  265. "div#watch7-headline",
  266. "div#watch-headline-title",
  267. // Vimeo
  268. ".clip_info-wrapper",
  269. ],
  270.  
  271. css_div: [
  272. "position: fixed",
  273. "top: 0",
  274. "right: 0",
  275. "zIndex: 100",
  276. "background: rgba(0, 0, 0, 0.8)",
  277. "color: #eeeeee",
  278. "padding: 10px"
  279. ].map(rule => rule.split(/: */)),
  280.  
  281. css_vsb_container: [
  282. "borderBottom: none",
  283. "marginBottom: 0",
  284. "paddingBottom: 0",
  285. ].map(rule => rule.split(/: */))
  286. };
  287.  
  288. function logvsb(where, msg, lvl = 0){
  289. let logf = (["info", "error"])[lvl];
  290.  
  291. console[logf](`[vsb::${where}] ${msg}`);
  292. }
  293.  
  294. function loader_loop(){
  295. let vsbc = () => document.querySelector(".vsb-container");
  296. let candidate;
  297. let default_candidate;
  298.  
  299. if(vsbc() !== null)
  300. return;
  301.  
  302. candidate = loader_data
  303. .container_candidates
  304. .map(candidate => document.querySelector(candidate))
  305. .find(candidate => candidate !== null);
  306.  
  307. default_candidate = (function(){
  308. let el = document.createElement("div");
  309.  
  310. loader_data.css_div.forEach(function([name, value]){
  311. el.style[name] = value; });
  312.  
  313. document.body.appendChild(el);
  314. return el;
  315. }());
  316.  
  317. video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
  318.  
  319. if(candidate === null){
  320. logvsb("loader_loop", "no candidates for title section. Defaulting to top of page.");
  321.  
  322. loader_data.css_vsb_container.forEach(function([name, value]){
  323. vsbc().style[name] = value;
  324. });
  325. }
  326. }
  327.  
  328. setInterval(function(){
  329. if(document.readyState === "complete")
  330. setTimeout(loader_loop, 1000);
  331. }, 1000); // Blame YouTube for this