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

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

当前为 2020-05-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.3
  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. function generateControl() {
  38. let app = document.createElement("div");
  39. let inputFrom = document.createElement("input");
  40. inputFrom.placeholder = "from time";
  41. let inputTo = document.createElement("input");
  42. inputTo.placeholder = "to time";
  43. let currentTime = document.createElement("span");
  44. let btn = document.createElement("button");
  45. let btnStop = document.createElement("button");
  46. let btnExport = document.createElement("button");
  47. applyStyle(app, {
  48. display: "flex",
  49. alignItems: "center",
  50. justifyContent: "space-between",
  51. maxWidth: "700px",
  52. marginTop: "15px",
  53. marginLeft: "auto",
  54. marginRight: "auto",
  55. });
  56. applyStyle(currentTime, {
  57. fontSize: "1.3rem",
  58. color: "var(--yt-spec-text-primary)",
  59. });
  60. let inputCommonStyle = {
  61. width: "120px",
  62. };
  63. applyStyle(inputFrom, inputCommonStyle);
  64. applyStyle(inputTo, inputCommonStyle);
  65. btn.innerText = "Repeat play";
  66. btnStop.innerText = "Stop";
  67. btnExport.innerText = "Export";
  68. app.appendChild(inputFrom);
  69. app.appendChild(inputTo);
  70. app.appendChild(currentTime);
  71. app.appendChild(btn);
  72. app.appendChild(btnStop);
  73. app.appendChild(btnExport);
  74. return {
  75. app,
  76. inputFrom,
  77. inputTo,
  78. currentTime,
  79. btn,
  80. btnStop,
  81. btnExport,
  82. };
  83. }
  84.  
  85. async function sleep(time) {
  86. await new Promise((resolve) => {
  87. setTimeout(() => {
  88. resolve();
  89. }, time);
  90. });
  91. }
  92.  
  93. async function main() {
  94. // Player fetching
  95. console.log("Waiting for the player...");
  96. let player;
  97. while (true) {
  98. player = document.querySelector("ytd-app #player");
  99. if (player && !player.hidden) break;
  100. await sleep(500);
  101. }
  102. let videoElement = document.querySelector("video");
  103. if (!videoElement || !player) {
  104. console.warn("Player not found. Exiting.");
  105. return;
  106. }
  107. console.log("Player detected.");
  108.  
  109. // Layout
  110. let control = generateControl();
  111. console.log(player);
  112. player.appendChild(control.app);
  113.  
  114. // States
  115. let fromValue = 0,
  116. toValue = 0;
  117.  
  118. // Initial state update attempt
  119. let urlTime = window.location.hash.match(
  120. /#pvp([0-9]+\.?[0-9]?)-([0-9]+\.?[0-9]?)/
  121. );
  122. if (urlTime !== null) {
  123. console.log("Attempting to recover time from URL...");
  124. control.inputFrom.value = fromValue = Number(urlTime[1]) || 0;
  125. control.inputTo.value = toValue = Number(urlTime[2]) || 0;
  126. }
  127.  
  128. // Current playback time
  129. function updateCurrentTime() {
  130. control.currentTime.innerText = Number(videoElement.currentTime).toFixed(2);
  131. requestAnimationFrame(updateCurrentTime);
  132. }
  133. requestAnimationFrame(updateCurrentTime);
  134.  
  135. // Repeat playback
  136. function onTimeUpdate() {
  137. if (videoElement.currentTime >= Number(toValue)) {
  138. videoElement.currentTime = Number(fromValue);
  139. }
  140. }
  141. control.btn.addEventListener("click", (evt) => {
  142. evt.preventDefault();
  143. videoElement.pause();
  144. videoElement.currentTime = fromValue;
  145. if (fromValue < toValue) {
  146. videoElement.play();
  147. videoElement.addEventListener("timeupdate", onTimeUpdate);
  148. } else {
  149. videoElement.removeEventListener("timeupdate", onTimeUpdate);
  150. }
  151. });
  152. control.btnStop.addEventListener("click", (evt) => {
  153. evt.preventDefault();
  154. videoElement.removeEventListener("timeupdate", onTimeUpdate);
  155. videoElement.pause();
  156. });
  157.  
  158. // Start/end time setting
  159. function updateURL() {
  160. history.pushState(null, null, `#pvp${fromValue}-${toValue}`);
  161. }
  162. control.inputFrom.addEventListener("change", () => {
  163. let input = control.inputFrom.value;
  164. if (input === "") {
  165. fromValue = 0;
  166. control.inputFrom.placeholder = "from 0";
  167. return;
  168. }
  169. let time = parseTime(input);
  170. if (time == -1) {
  171. control.btn.disabled = true;
  172. return;
  173. }
  174. control.btn.disabled = false;
  175. fromValue = time;
  176. updateURL();
  177. });
  178. control.inputTo.addEventListener("change", () => {
  179. let input = control.inputTo.value;
  180. if (input === "") {
  181. toValue = videoElement.duration || 0;
  182. control.inputTo.placeholder = `to ${toValue.toFixed(2)}`;
  183. return;
  184. }
  185. let time = parseTime(input);
  186. if (time == -1) {
  187. control.btn.disabled = true;
  188. return;
  189. }
  190. control.btn.disabled = false;
  191. toValue = time;
  192. updateURL();
  193. });
  194.  
  195. // Button export
  196. control.btnExport.addEventListener("click", (evt) => {
  197. evt.preventDefault();
  198. let videoId = getVideoId(window.location);
  199. alert(`ffmpeg -i $(youtube-dl -f bestaudio -g "https://www.youtube.com/watch?v=${videoId}") \
  200. -ss ${fromValue} \
  201. -to ${toValue} \
  202. -acodec libmp3lame \
  203. -ab 192k \
  204. -af loudnorm=I=-16:TP=-2:LRA=11 \
  205. -vn \
  206. output-${videoId}-${fromValue}-${toValue}.mp3`);
  207. });
  208. }
  209.  
  210. main();