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

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

目前为 2020-06-28 提交的版本,查看 最新版本

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