精确控制视频播放进度 (YouTube)

精确控制视频播放进度/生成剪辑脚本的工具栏

目前为 2020-04-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Precise video playback (YouTube)
  3. // @name:zh-CN 精确控制视频播放进度 (YouTube)
  4. // @description A toolbar to set precise video play time and generate clip script
  5. // @description:zh-CN 精确控制视频播放进度/生成剪辑脚本的工具栏
  6. // @namespace moe.suisei.pvp.youtube
  7. // @match https://www.youtube.com/watch*
  8. // @grant none
  9. // @version 0.5.1
  10. // @author Outvi V
  11. // ==/UserScript==
  12.  
  13. "use strict";
  14.  
  15. console.log("Precise Video Playback is up");
  16.  
  17. function getVideoId(url) {
  18. return String(url).match(/v=([^&#]+)/)[1];
  19. }
  20.  
  21. function applyStyle(elem, styles) {
  22. for (const [key, value] of Object.entries(styles)) {
  23. elem.style[key] = value;
  24. }
  25. }
  26.  
  27. function parseTime(str) {
  28. if (!isNaN(Number(str))) return Number(str);
  29. let time = str.match(/([0-9]?)?:([0-9]+)(\.([0-9]+))?/);
  30. if (time === null) return -1;
  31. let ret =
  32. Number(time[1] || 0) * 60 + Number(time[2]) + Number(time[4] || 0) * 0.1;
  33. if (ret == NaN) return -1;
  34. return ret;
  35. }
  36.  
  37. parseTime("0:4.2");
  38.  
  39. function generateControl() {
  40. let app = document.createElement("div");
  41. let inputFrom = document.createElement("input");
  42. inputFrom.placeholder = "from time";
  43. let inputTo = document.createElement("input");
  44. inputTo.placeholder = "to time";
  45. let currentTime = document.createElement("span");
  46. let btn = document.createElement("button");
  47. let btnStop = document.createElement("button");
  48. let btnExport = document.createElement("button");
  49. applyStyle(app, {
  50. display: "flex",
  51. alignItems: "center",
  52. justifyContent: "space-between",
  53. maxWidth: "700px",
  54. marginTop: "15px",
  55. marginLeft: "auto",
  56. marginRight: "auto",
  57. });
  58. applyStyle(currentTime, {
  59. fontSize: "1.3rem",
  60. });
  61. let inputCommonStyle = {
  62. width: "120px",
  63. };
  64. applyStyle(inputFrom, inputCommonStyle);
  65. applyStyle(inputTo, inputCommonStyle);
  66. btn.innerText = "Repeat play";
  67. btnStop.innerText = "Stop";
  68. btnExport.innerText = "Export";
  69. app.appendChild(inputFrom);
  70. app.appendChild(inputTo);
  71. app.appendChild(currentTime);
  72. app.appendChild(btn);
  73. app.appendChild(btnStop);
  74. app.appendChild(btnExport);
  75. return {
  76. app,
  77. inputFrom,
  78. inputTo,
  79. currentTime,
  80. btn,
  81. btnStop,
  82. btnExport,
  83. };
  84. }
  85.  
  86. async function sleep(time) {
  87. await new Promise((resolve) => {
  88. setTimeout(() => {
  89. resolve();
  90. }, time);
  91. });
  92. }
  93.  
  94. async function main() {
  95. // Player fetching
  96. console.log("Waiting for the player...");
  97. let player;
  98. while (true) {
  99. player = document.querySelector("ytd-app #player");
  100. if (player && !player.hidden) break;
  101. await sleep(500);
  102. }
  103. let videoElement = document.querySelector("video");
  104. if (!videoElement || !player) {
  105. console.warn("Player not found. Exiting.");
  106. return;
  107. }
  108. console.log("Player detected.");
  109.  
  110. // Layout
  111. let control = generateControl();
  112. console.log(player);
  113. player.appendChild(control.app);
  114.  
  115. // States
  116. let fromValue = 0,
  117. toValue = 0;
  118.  
  119. // Initial state update attempt
  120. let urlTime = window.location.hash.match(
  121. /#pvp([0-9]+\.?[0-9]?)-([0-9]+\.?[0-9]?)/
  122. );
  123. if (urlTime !== null) {
  124. console.log("Attempting to recover time from URL...");
  125. control.inputFrom.value = fromValue = Number(urlTime[1]) || 0;
  126. control.inputTo.value = toValue = Number(urlTime[2]) || 0;
  127. }
  128.  
  129. // Current playback time
  130. function updateCurrentTime() {
  131. control.currentTime.innerText = Number(videoElement.currentTime).toFixed(2);
  132. requestAnimationFrame(updateCurrentTime);
  133. }
  134. requestAnimationFrame(updateCurrentTime);
  135.  
  136. // Repeat playback
  137. function onTimeUpdate() {
  138. if (videoElement.currentTime >= Number(toValue)) {
  139. videoElement.currentTime = Number(fromValue);
  140. }
  141. }
  142. control.btn.addEventListener("click", (evt) => {
  143. evt.preventDefault();
  144. videoElement.pause();
  145. videoElement.currentTime = fromValue;
  146. if (fromValue < toValue) {
  147. videoElement.play();
  148. videoElement.addEventListener("timeupdate", onTimeUpdate);
  149. } else {
  150. videoElement.removeEventListener("timeupdate", onTimeUpdate);
  151. }
  152. });
  153. control.btnStop.addEventListener("click", (evt) => {
  154. evt.preventDefault();
  155. videoElement.removeEventListener("timeupdate", onTimeUpdate);
  156. videoElement.pause();
  157. });
  158.  
  159. // Start/end time setting
  160. function updateURL() {
  161. history.pushState(null, null, `#pvp${fromValue}-${toValue}`);
  162. }
  163. control.inputFrom.addEventListener("change", () => {
  164. let input = control.inputFrom.value;
  165. if (input === "") {
  166. fromValue = 0;
  167. control.inputFrom.placeholder = "from 0";
  168. return;
  169. }
  170. let time = parseTime(input);
  171. if (time == -1) {
  172. control.btn.disabled = true;
  173. return;
  174. }
  175. control.btn.disabled = false;
  176. fromValue = time;
  177. updateURL();
  178. });
  179. control.inputTo.addEventListener("change", () => {
  180. let input = control.inputTo.value;
  181. if (input === "") {
  182. toValue = videoElement.duration || 0;
  183. control.inputTo.placeholder = `to ${toValue.toFixed(2)}`;
  184. return;
  185. }
  186. let time = parseTime(input);
  187. if (time == -1) {
  188. control.btn.disabled = true;
  189. return;
  190. }
  191. control.btn.disabled = false;
  192. toValue = time;
  193. updateURL();
  194. });
  195.  
  196. // Button export
  197. control.btnExport.addEventListener("click", (evt) => {
  198. evt.preventDefault();
  199. let videoId = getVideoId(window.location);
  200. alert(`ffmpeg -i $(youtube-dl -f bestaudio -g "https://www.youtube.com/watch?v=${videoId}") \
  201. -ss ${fromValue} \
  202. -to ${toValue} \
  203. -acodec libmp3lame \
  204. -ab 192k \
  205. -af loudnorm=I=-16:TP=-2:LRA=11 \
  206. output-${videoId}-${fromValue}-${toValue}.mp3`);
  207. });
  208. }
  209.  
  210. main();