Video Speed Buttons

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

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

  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.2
  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. const container = (function(){
  80. var div = document.createElement("div");
  81. var prev_node = null;
  82.  
  83. div.className = "vsb-container";
  84. div.style.borderBottom = "1px solid #ccc";
  85. div.style.marginBottom = "10px";
  86. div.style.paddingBottom = "10px";
  87. div.appendChild(document.createTextNode(LABEL_TEXT));
  88.  
  89. BUTTON_TEMPLATES.forEach(function(button){
  90. var speedButton = SpeedButton(...button, div);
  91.  
  92. if(buttons.head === null)
  93. buttons.head = speedButton;
  94.  
  95. if(prev_node !== null){
  96. speedButton.prev = prev_node;
  97. prev_node.next = speedButton;
  98. }
  99.  
  100. prev_node = speedButton;
  101.  
  102. if(speedButton.speed == DEFAULT_SPEED)
  103. speedButton.select();
  104. });
  105.  
  106. return div;
  107. })();
  108.  
  109. function is_comment_box(el){
  110. return el === document.querySelector(".comment-simplebox-text");
  111. }
  112.  
  113. function Infobox(parent){
  114. var el = document.createElement("pre");
  115.  
  116. el.style.font = "1em monospace";
  117. el.style.borderTop = "1px solid #ccc";
  118. el.style.marginTop = "10px";
  119. el.style.paddingTop = "10px";
  120.  
  121. el.addEventListener("click", function(){
  122. parent.removeChild(el);
  123. });
  124.  
  125. parent.appendChild(el);
  126.  
  127. function log(msg){
  128. el.innerHTML += msg;
  129. }
  130.  
  131. return {
  132. el,
  133. log
  134. };
  135. }
  136.  
  137. function setPlaybackRate(el, rate){
  138. if(el)
  139. el.playbackRate = rate;
  140. else
  141. console.log("setPlaybackRate: video element is null or undefined");
  142. }
  143.  
  144. function SpeedButton(text, speed, parent){
  145. var el = document.createElement("span");
  146. var self;
  147.  
  148. el.innerHTML = text;
  149. el.style.marginRight = "10px";
  150. el.style.fontWeight = "bold";
  151. el.style.fontSize = BUTTON_SIZE;
  152. el.style.color = COLOR_NORMAL;
  153. el.style.cursor = "pointer";
  154.  
  155. el.addEventListener("click", function(){
  156. setPlaybackRate(video_el, speed);
  157. self.select();
  158. });
  159.  
  160. parent.appendChild(el);
  161.  
  162. function select(){
  163. if(buttons.last !== null)
  164. buttons.last.el.style.color = COLOR_NORMAL;
  165.  
  166. buttons.last = self;
  167. buttons.selected = self;
  168. el.style.color = COLOR_SELECTED;
  169. }
  170.  
  171. function getprev(){
  172. if(self.prev === null)
  173. return self;
  174.  
  175. return buttons.selected = self.prev;
  176. }
  177.  
  178. function getnext(){
  179. if(self.next === null)
  180. return self;
  181.  
  182. return buttons.selected = self.next;
  183. }
  184.  
  185. return self = {
  186. el,
  187. text,
  188. speed,
  189. prev: null,
  190. next: null,
  191. select,
  192. getprev,
  193. getnext
  194. };
  195. }
  196.  
  197. function kill(){
  198. anchor.removeChild(container);
  199. document.body.removeEventListener("keydown", ev_keyboard);
  200. }
  201.  
  202. function ev_keyboard(ev){
  203. let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
  204. let callback = (match || {2: ()=>null})[2];
  205.  
  206. callback(ev);
  207. }
  208.  
  209. setPlaybackRate(video_el, DEFAULT_SPEED);
  210. anchor.insertBefore(container, anchor.firstChild);
  211. document.body.addEventListener("keydown", ev_keyboard);
  212.  
  213. return {
  214. controls: keyboard_controls,
  215. buttons,
  216. kill,
  217. SpeedButton,
  218. Infobox,
  219. setPlaybackRate,
  220. is_comment_box
  221. };
  222. }
  223.  
  224. video_speed_buttons.from_query = function(anchor_q, video_q){
  225. return video_speed_buttons(
  226. document.querySelector(anchor_q),
  227. document.querySelector(video_q));
  228. }
  229.  
  230. // Multi-purpose Loader (defaults to floating on top right)
  231. const loader_data = {
  232. container_candidates: [
  233. // YouTube
  234. "div#container.ytd-video-primary-info-renderer",
  235. "div#watch-header",
  236. "div#watch7-headline",
  237. "div#watch-headline-title",
  238. // Vimeo
  239. ".clip_info-wrapper",
  240. ],
  241.  
  242. css_div: [
  243. "position: fixed",
  244. "top: 0",
  245. "right: 0",
  246. "zIndex: 100",
  247. "background: rgba(0, 0, 0, 0.8)",
  248. "color: #eeeeee",
  249. "padding: 10px"
  250. ].map(rule => rule.split(/: */)),
  251.  
  252. css_vsb_container: [
  253. "borderBottom: none",
  254. "marginBottom: 0",
  255. "paddingBottom: 0",
  256. ].map(rule => rule.split(/: */))
  257. };
  258.  
  259. setInterval(function(){
  260. let vsbc = () => document.querySelector(".vsb-container");
  261. let candidate;
  262. let default_candidate;
  263.  
  264. if(document.readyState !== "complete" || vsbc() !== null)
  265. return;
  266.  
  267. candidate = loader_data
  268. .container_candidates
  269. .map(candidate => document.querySelector(candidate))
  270. .find(candidate => candidate !== null);
  271.  
  272. default_candidate = (function(){
  273. let el = document.createElement("div");
  274.  
  275. loader_data.css_div.forEach(function([name, value]){
  276. el.style[name] = value; });
  277.  
  278. document.body.appendChild(el);
  279. return el;
  280. }());
  281.  
  282. video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
  283.  
  284. if(!candidate)
  285. loader_data.css_vsb_container.forEach(function([name, value]){
  286. vsbc().style[name] = value; });
  287. }, 100);