Video Speed Buttons

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

目前为 2020-02-29 提交的版本。查看 最新版本

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