Video Speed Buttons

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

目前为 2024-05-21 提交的版本。查看 最新版本

  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.3
  6. // @copyright 2017, Braden Best
  7. // @run-at document-end
  8. // @author Braden Best
  9. // @grant none
  10. //
  11. // @match *://*.youtube.com/*
  12. // @match *://youtube.com/*
  13. // @match *://*.vimeo.com/*
  14. // @match *://vimeo.com/*
  15. // ==/UserScript==
  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. let playbackRate_data = {
  170. rate: 1,
  171. video: null,
  172. };
  173.  
  174. function setPlaybackRate(el, rate){
  175. if(el) {
  176. el.playbackRate = rate;
  177. playbackRate_data.rate = rate;
  178. playbackRate_data.video = el;
  179. }
  180. else
  181. logvsb("video_speed_buttons::setPlaybackRate", "video element is null or undefined", 1);
  182. }
  183.  
  184. function SpeedButtonLabel(text){
  185. let el = document.createElement("span");
  186.  
  187. el.innerHTML = text;
  188. el.style.marginRight = "10px";
  189. el.style.fontWeight = "bold";
  190. el.style.fontSize = BUTTON_SIZE;
  191. el.style.color = COLOR_NORMAL;
  192.  
  193. return el;
  194. }
  195.  
  196. function SpeedButton(text, speed, parent){
  197. let el = SpeedButtonLabel(text);
  198. let self;
  199.  
  200. el.style.cursor = "pointer";
  201.  
  202. el.addEventListener("click", function(){
  203. setPlaybackRate(video_el, speed);
  204. self.select();
  205. });
  206.  
  207. parent.appendChild(el);
  208.  
  209. function select(){
  210. if(buttons.last !== null)
  211. buttons.last.el.style.color = COLOR_NORMAL;
  212.  
  213. buttons.last = self;
  214. buttons.selected = self;
  215. el.style.color = COLOR_SELECTED;
  216. }
  217.  
  218. function getprev(){
  219. if(self.prev === null)
  220. return self;
  221.  
  222. return buttons.selected = self.prev;
  223. }
  224.  
  225. function getnext(){
  226. if(self.next === null)
  227. return self;
  228.  
  229. return buttons.selected = self.next;
  230. }
  231.  
  232. return self = {
  233. el,
  234. text,
  235. speed,
  236. prev: null,
  237. next: null,
  238. select,
  239. getprev,
  240. getnext
  241. };
  242. }
  243.  
  244. function kill(){
  245. anchor.removeChild(container);
  246. document.body.removeEventListener("keydown", ev_keyboard);
  247. }
  248.  
  249. function set_video_el(new_video_el){
  250. video_el = new_video_el;
  251. }
  252.  
  253. function ev_keyboard(ev){
  254. let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
  255. let callback = (match || {2: ()=>null})[2];
  256.  
  257. callback(ev);
  258. }
  259.  
  260. setPlaybackRate(video_el, DEFAULT_SPEED);
  261. anchor.insertBefore(container, anchor.firstChild);
  262. document.body.addEventListener("keydown", ev_keyboard);
  263.  
  264. return {
  265. controls: keyboard_controls,
  266. buttons,
  267. kill,
  268. SpeedButton,
  269. Infobox,
  270. setPlaybackRate,
  271. is_comment_box,
  272. set_video_el,
  273. playbackRate_data,
  274. ALLOW_EXTERNAL_ACCESS,
  275. };
  276. }
  277.  
  278. video_speed_buttons.from_query = function(anchor_q, video_q){
  279. return video_speed_buttons(
  280. document.querySelector(anchor_q),
  281. document.querySelector(video_q));
  282. }
  283.  
  284. // Multi-purpose Loader (defaults to floating on top right)
  285. const loader_data = {
  286. container_candidates: [
  287. // YouTube
  288. "div#container.ytd-video-primary-info-renderer",
  289. "div#watch-header",
  290. "div#watch7-headline",
  291. "div#watch-headline-title",
  292. "div#title.style-scope.ytd-watch-metadata",
  293. // Vimeo
  294. ".clip_info-wrapper",
  295. ],
  296.  
  297. css_div: [
  298. "position: fixed",
  299. "top: 0",
  300. "right: 0",
  301. "zIndex: 100000",
  302. "background: rgba(0, 0, 0, 0.8)",
  303. "color: #eeeeee",
  304. "padding: 10px"
  305. ].map(rule => rule.split(/: */)),
  306.  
  307. css_vsb_container: [
  308. "borderBottom: none",
  309. "marginBottom: 0",
  310. "paddingBottom: 0",
  311. ].map(rule => rule.split(/: */))
  312. };
  313.  
  314. function logvsb(where, msg, lvl = 0){
  315. let logf = (["info", "error"])[lvl];
  316.  
  317. console[logf](`[vsb::${where}] ${msg}`);
  318. }
  319.  
  320. function loader_loop(){
  321. let vsbc = () => document.querySelector(".vsb-container");
  322. let candidate;
  323. let default_candidate;
  324. let vsb_handle;
  325.  
  326. if(vsbc() !== null)
  327. return;
  328.  
  329. candidate = loader_data
  330. .container_candidates
  331. .map(candidate => document.querySelector(candidate))
  332. .find(candidate => candidate !== null);
  333.  
  334. default_candidate = (function(){
  335. let el = document.createElement("div");
  336.  
  337. loader_data.css_div.forEach(function([name, value]){
  338. el.style[name] = value; });
  339.  
  340. return el;
  341. }());
  342.  
  343. vsb_handle = video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
  344.  
  345. if(candidate === null){
  346. logvsb("loader_loop", "no candidates for title section. Defaulting to top of page.");
  347. document.body.appendChild(default_candidate);
  348.  
  349. loader_data.css_vsb_container.forEach(function([name, value]){
  350. vsbc().style[name] = value;
  351. });
  352. }
  353.  
  354. // ugly hack to address vimeo automatically resetting the speed
  355. vsb_handle.enforcer_loop_iid = setInterval(function(){
  356. let prdata = vsb_handle.playbackRate_data;
  357.  
  358. if (prdata.video !== null)
  359. prdata.video.playbackRate = prdata.rate;
  360. }, 500);
  361.  
  362. if(vsb_handle.ALLOW_EXTERNAL_ACCESS)
  363. window.vsb = vsb_handle;
  364. }
  365.  
  366. setInterval(function(){
  367. if(document.readyState === "complete")
  368. setTimeout(loader_loop, 1000);
  369. }, 1000); // Blame YouTube for this