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