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