HTML5 Audio/Video Keyboard Shortcuts With OSD

Adds keyboard shortcuts for controlling HTML5 media player (audio/video) with OSD support. Seek media to 0%, 5%, 10%, ..., or 95%. Rewind and fast fordward media by 30 seconds, 1 minute, and 5 minutes. Change media speed even beyond YouTube's speed limit. Change audio volume to 20%, 40%, 60%, 80%, or 100%. Change video aspect ratio for TV and letterbox content (for widescreen monitors).

目前為 2024-02-20 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name HTML5 Audio/Video Keyboard Shortcuts With OSD
  3. // @namespace https://greasyfork.org/en/users/85671-jcunews
  4. // @version 1.2.16
  5. // @license AGPLv3
  6. // @author jcunews
  7. // @description Adds keyboard shortcuts for controlling HTML5 media player (audio/video) with OSD support. Seek media to 0%, 5%, 10%, ..., or 95%. Rewind and fast fordward media by 30 seconds, 1 minute, and 5 minutes. Change media speed even beyond YouTube's speed limit. Change audio volume to 20%, 40%, 60%, 80%, or 100%. Change video aspect ratio for TV and letterbox content (for widescreen monitors).
  8. // @match *://*/*
  9. // @grant none
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. /*
  14. Notes:
  15.  
  16. - Some shortcuts won't work on non US keyboards. Non US keyboard users will need to manually edit the keys in the script.
  17. - In YouTube, if the video speed is below 0.25x or above 2x, the YouTube setting display will be capped to 0.1x or 2x.
  18. - Web browser video speeds: Firefox = 0.25 to 5.0; Chrome = 0.1 to 16.0.
  19.  
  20.  
  21. Keyboard Shortcuts:
  22.  
  23. CTRL+, = Rewind media by 1/30th second
  24. CTRL+. = Fast forward media by 1/30th second
  25. CTRL+SHIFT+/ = Next frame (when paused; Firefox only)
  26. SHIFT+LEFT = Rewind media by 30 seconds
  27. SHIFT+RIGHT = Fast forward media by 30 seconds
  28. CTRL+LEFT = Rewind media by 1 minute
  29. CTRL+RIGHT = Fast forward media by 1 minute
  30. CTRL+SHIFT+LEFT = Rewind media by 5 minutes
  31. CTRL+SHIFT+RIGHT = Fast forward media by 5 minutes
  32. CTRL+/ = Fast forward media by 1.5 minutes
  33. 0 to 9 = Seek media to 0%, 10%, 20%,...90%
  34. SHIFT+0 to SHIFT+9 = Seek media to 5%, 15%, 25%,...95%
  35. CTRL+1 to CTRL+5 = Change audio volume to 20%, 40%, 60$, 80%, 100%
  36. CTRL+[ = Decrease media speed by 0.2x (by default)
  37. CTRL+] = Increase media speed by 0.2x (by default)
  38. CTRL+; = Reset media speed
  39. CTRL+' = Change custom media speed
  40. CTRL+\ = Change unit of media speed increment/decrement
  41.  
  42. For Widescreen Video Viewport:
  43. CTRL+6 = Change video aspect ratio for widescreen content. Fix widescreen content shrunk to 4:3 TV format.
  44. CTRL+7 = Change video aspect ratio for letterbox content. Fix 4:3 letterbox content stretched to widescreen format.
  45. CTRL+8 = Change video aspect ratio for TV content. Fix 4:3 TV content stretched to widescreen format.
  46.  
  47. For 4:3 TV Video Viewport:
  48. CTRL+SHIFT+6 = Change video aspect ratio for ultra widescreen content. Fix ultra widescreen content compressed into 4:3 TV format.
  49. CTRL+SHIFT+7 = Zoom 4:3 letterbox content to remove half of top+bottom borders, but also remove left+right content a little.
  50. This can also be used to half-zoom ultra widescreen content on widescreen viewport. i.e. half-zoom of CTRL+6.
  51. CTRL+SHIFT+8 = Change video aspect ratio for widescreen content. Fix widescreen content compressed into 4:3 TV format.
  52.  
  53. For Any Video Viewport:
  54. CTRL+9 = Reset video aspect ratio
  55. ALT+S = Take screenshot if current video frame (in its original size and aspect ratio)
  56. */
  57.  
  58. ((eleOSD, osdTimer) => {
  59.  
  60. //=== CONFIGURATION BEGIN ===
  61.  
  62. //Video speed increment/decrement unit.
  63. var incrementUnit = 0.2;
  64.  
  65. //Duration (in milliseconds) to display On Screen Display (OSD) when changing playback rate. Set to zero or less to disable.
  66. var osdTimeout = 3000;
  67.  
  68. //Image format for video frame screenshot
  69. var imageFormat = "jpeg"; //can be jpeg or png
  70.  
  71. //Keyboard shortcuts.
  72. //key = Key name. String type if single shortcut, or array of string if multiple shortcut (for single function multiple shortcuts).
  73. // Each key name can either be the uppercased character which is produced by the key (e.g. `A`, `4`, `*`, etc.),
  74. // or the code name for the key (e.g. `Digit2`, `BracketLeft`, etc.). Both types are case sensitive.
  75. // When SHIFT modifier is used with keys which produces a character, key code name should be used.
  76. // A list of key code names can be found here: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
  77. //modifiers = Any combinations of "C", "S", and "A", for Ctrl, Shift, and Alt keys.
  78. var keys = [
  79. { //ctrl+space: seek media to next frame (only when paused. firefox only)
  80. key: " ", modifiers: "C",
  81. func: (key, ele) => ele.seekToNextFrame && ele.seekToNextFrame()
  82. },
  83. { //0 to 9: seek media to 0%,10%,20%,...90%
  84. key: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], modifiers: "",
  85. func: (ele, key, keyIndex) => ele.currentTime = keyIndex / 10 * ele.duration
  86. },
  87. { //shift+0 to shift+9: seek media to 5%,15%,25%,...95%
  88. key: [")", "!", "@", "#", "$", "%", "^", "&", "*", "("], modifiers: "S",
  89. func: (ele, key, keyIndex) => ele.currentTime = (keyIndex + 0.5) / 10 * ele.duration
  90. },
  91. { //ctrl+1 to ctrl+5: set audio volume to 20%,40%,60%,80%,100%
  92. key: ["1", "2", "3", "4", "5"], modifiers: "C",
  93. func: (ele, key, keyIndex) => updAudioVolume(ele, (parseInt(key) * 2) / 10)
  94. },
  95. { //shift+left: rewind media by 30 seconds
  96. key: "ArrowLeft", modifiers: "S",
  97. func: (ele, key) => ele.currentTime -= 30
  98. },
  99. { //ctrl+left: rewind media by 1 minute
  100. key: "ArrowLeft", modifiers: "C",
  101. func: (ele, key) => ele.currentTime -= 60
  102. },
  103. { //ctrl+shift+left: rewind media by 5 minutes
  104. key: "ArrowLeft", modifiers: "CS",
  105. func: (ele, key) => ele.currentTime -= 300
  106. },
  107. { //ctrl+,: rewind media by 1/30 second
  108. key: ",", modifiers: "C",
  109. func: (ele, key) => ele.currentTime -= 1/30
  110. },
  111. { //shift+right: fast forward media by 30 seconds
  112. key: "ArrowRight", modifiers: "S",
  113. func: (ele, key) => ele.currentTime += 30
  114. },
  115. { //ctrl+right: fast forward media by 1 minute
  116. key: "ArrowRight", modifiers: "C",
  117. func: (ele, key) => ele.currentTime += 60
  118. },
  119. { //ctrl+shift+right: fast forward media by 5 minutes
  120. key: "ArrowRight", modifiers: "CS",
  121. func: (ele, key) => ele.currentTime += 300
  122. },
  123. { //ctrl+.: fast forward media by 1/30th second
  124. key: ".", modifiers: "C",
  125. func: (ele, key) => ele.currentTime += 1/30
  126. },
  127. { //ctrl+shift+/: next frame (when paused; firefox only)
  128. key: "?", modifiers: "CS",
  129. func: (ele, key) => ele.seekToNextFrame && ele.seekToNextFrame()
  130. },
  131. { //ctrl+/: fast forward media by 1.5 minutes
  132. key: "/", modifiers: "C",
  133. func: (ele, key) => ele.currentTime += 87
  134. },
  135. { //ctrl+[: decrease media speed
  136. key: "[", modifiers: "C",
  137. func: (ele, key) => {
  138. key = ele.playbackRate - incrementUnit;
  139. if (key < 0.1) {
  140. key = 0.1;
  141. } else if ((key < 1) && (ele.playbackRate > 1)) key = 1;
  142. updVideoSpeed(ele, key);
  143. }
  144. },
  145. { //ctrl+]: increase media speed
  146. key: "]", modifiers: "C",
  147. func: (ele, key) => {
  148. key = ele.playbackRate + incrementUnit;
  149. if (key > 16) {
  150. key = 16;
  151. } else if ((key > 1) && (ele.playbackRate < 1)) key = 1;
  152. updVideoSpeed(ele, key);
  153. }
  154. },
  155. { //ctrl+;: reset media speed to 1x
  156. key: ";", modifiers: "C",
  157. func: (ele, key) => updVideoSpeed(ele, 1)
  158. },
  159. { //ctrl+': use custom media speed
  160. key: "'", modifiers: "C",
  161. func: (ele, key) => {
  162. if ((key = prompt("Enter media speed from 0.1 to 16 (inclusive).\ne.g.: 1 = Normal, 0.5 = Half, 2 = Double, 3 = Triple, etc.", ele.playbackRate)) === null) return;
  163. if (isNaN(key = parseFloat(key.trim()))) {
  164. alert("Input must be a number.");
  165. return;
  166. }
  167. updVideoSpeed(ele, (key = parseFloat(key.toFixed(1))) < 0.1 ? 0.1 : (key > 16 ? 16 : key));
  168. }
  169. },
  170. { //ctrl+\: change unit of media speed increment/decrement
  171. key: "\\", modifiers: "C",
  172. func: (ele, key) => {
  173. if ((key = prompt("Enter unit of media speed increment/decrement from 0.1 to 4 (inclusive).", incrementUnit)) === null) return;
  174. if (!isNaN(key = parseFloat(key.trim()))) {
  175. incrementUnit = (key = parseFloat(key.toFixed(1))) < 0.1 ? 0.1 : (key > 4 ? 4 : key);
  176. } else alert("Input must be a number.");
  177. }
  178. },
  179. { //ctrl+6: Widescreen aspect ratio
  180. key: "6", modifiers: "C", videoOnly: true,
  181. func: (ele, key) => updVideoAspect("scaleX(1.3333)", "Widescreen")
  182. },
  183. { //ctrl+7: Letterbox aspect ratio
  184. key: "7", modifiers: "C", videoOnly: true,
  185. func: (ele, key) => updVideoAspect("scaleY(1.3333)", "Letterbox")
  186. },
  187. { //ctrl+8: TV aspect ratio
  188. key: "8", modifiers: "C", videoOnly: true,
  189. func: (ele, key) => updVideoAspect("scaleX(0.75)", "TV")
  190. },
  191. { //ctrl+shift+6: Ultra widescreen aspect ratio
  192. key: "Digit6", modifiers: "CS", videoOnly: true,
  193. func: (ele, key) => updVideoAspect("scaleY(0.7168)", "Ultra Widescreen")
  194. },
  195. { //ctrl+shift+7: Half-zoom letterbox
  196. key: "Digit7", modifiers: "CS", videoOnly: true,
  197. func: (ele, key) => updVideoAspect("scale(1.1666)", "Letterbox Half-Zoom")
  198. },
  199. { //ctrl+shift+8: Widescreen on TV
  200. key: "Digit8", modifiers: "CS", videoOnly: true,
  201. func: (ele, key) => updVideoAspect("scaleY(0.5625)", "Widescreen On TV")
  202. },
  203. { //ctrl+9: reset video aspect ratio
  204. key: "9", modifiers: "C", videoOnly: true,
  205. func: (ele, key) => updVideoAspect("", "Reset")
  206. },
  207. { //alt+s: take screenshot of current video frame
  208. key: "S", modifiers: "A", videoOnly: true,
  209. func: (ele, key, cv, a) => {
  210. cv = document.createElement("CANVAS");
  211. if (cv.width = ele.videoWidth) {
  212. cv.height = ele.videoHeight;
  213. cv.getContext("2d").drawImage(ele, 0, 0);
  214. a = document.createElement("A");
  215. a.href = cv.toDataURL("image/" + imageFormat);
  216. a.download = `video_frame_${ele.currentTime}.${imageFormat === "jpeg" ? "jpg" : imageFormat}`;
  217. a.style.display = "none";
  218. document.body.appendChild(a).click();
  219. return a.remove()
  220. }
  221. }
  222. }
  223. ];
  224. keys.forEach((k, s, m) => {
  225. if (Array.isArray(k.key)) {
  226. k.key.forEach((s, i) => k.key[i] = k.key[i].toUpperCase());
  227. } else k.key = k.key.toUpperCase();
  228. s = k.modifiers.toUpperCase();
  229. k.modifiers = {ctrl: s.includes("C"), shift: s.includes("S"), alt: s.includes("A")};
  230. });
  231.  
  232. //=== CONFIGURATION END ===
  233.  
  234. function showOSD(s) {
  235. if (osdTimeout < 0) return;
  236. if (eleOSD) {
  237. eleOSD.textContent = s;
  238. } else {
  239. eleOSD = document.createElement("DIV");
  240. eleOSD.style.cssText = "position:fixed;z-index:999999999;right:.5rem;bottom:.5rem;margin:0;padding:.2rem .5rem .1rem .5rem;width:auto;height:auto;font:normal 16pt/normal sans-serif;background:#444;color:#fff";
  241. eleOSD.textContent = s;
  242. document.body.appendChild(eleOSD);
  243. }
  244. clearTimeout(osdTimer);
  245. osdTimer = setTimeout(() => {
  246. eleOSD.remove();
  247. eleOSD = null;
  248. }, osdTimeout);
  249. }
  250.  
  251. function stopEvent(ev) {
  252. ev.preventDefault();
  253. ev.stopPropagation();
  254. ev.stopImmediatePropagation();
  255. }
  256.  
  257. function updVideoSpeed(ele, spd, e) {
  258. if ((location.hostname === "www.youtube.com") && (e = ele.parentNode.parentNode).setPlaybackRate && (spd >= 0.25) && (spd <= 2)) {
  259. e.setPlaybackRate(spd = parseFloat(spd.toFixed(1)));
  260. } else ele.playbackRate = spd = parseFloat(spd.toFixed(1));
  261. showOSD("Speed " + spd + "x");
  262. }
  263.  
  264. function updVideoAspect(asp, label, s) {
  265. if (!(s = document.getElementById("vidAspOvr"))) document.body.appendChild(s = document.createElement("STYLE")).id = "vidAspOvr";
  266. s.innerHTML = asp ? `video{transform:${asp}!important}` : "";
  267. showOSD("Ratio: " + label);
  268. }
  269.  
  270. function updAudioVolume(ele, vol, e) {
  271. if ((location.hostname === "www.youtube.com") && (e = ele.parentNode.parentNode).setVolume) {
  272. e.setVolume(vol * 100);
  273. } else ele.volume = vol;
  274. showOSD("Audio " + (vol * 100) + "%");
  275. }
  276.  
  277. function isVisible(ele) {
  278. while (ele && ele.tagName) {
  279. if (getComputedStyle(ele).display === "none") return false;
  280. ele = ele.parentNode
  281. }
  282. return true
  283. }
  284.  
  285. incrementUnit = parseFloat((incrementUnit < 0.1 ? 0.1 : (incrementUnit > 1 ? 1 : incrementUnit)).toFixed(1));
  286. addEventListener("keydown", function(ev, ele) {
  287. if ((!(ele = document.activeElement) || !((ele.contentEditable === "true") || ["BUTTON", "INPUT", "SELECT", "TEXTAREA"].includes(ele.tagName))) && (ele = document.querySelector("video,audio"))) {
  288. keys.some((k, a, i) => {
  289. a = Array.isArray(k.key);
  290. if (
  291. ((!a && ((k.key === ev.key.toUpperCase()) || (k.key === ev.code))) || (a && (((i = k.key.indexOf(ev.key)) >= 0) || ((i = k.key.indexOf(ev.code)) >= 0)))) &&
  292. (k.modifiers.ctrl === ev.ctrlKey) && (k.modifiers.shift === ev.shiftKey) && (k.modifiers.alt === ev.altKey) &&
  293. (!k.videoOnly || (ele.tagName === "VIDEO")) && (isVisible(ele) || (ele.tagName === "AUDIO"))
  294. ) {
  295. stopEvent(ev);
  296. k.func(ele, ev.key, a ? i : null);
  297. return true;
  298. }
  299. });
  300. }
  301. }, true);
  302.  
  303. })();