视频倍速播放

长按右方向键倍速播放,松开恢复原速,按 + 键增加倍速,按 - 键减少倍速,单击右方向键快进5秒。 按 ] 键从 1.5 倍速开始,每按一次增加 0.5 倍速,按 [ 键每次减少 0.5 倍速,按P键恢复1.0倍速。适配大部分网页播放器,尤其适配jellyfin等播放器播放nas内容。

目前為 2025-03-09 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name 视频倍速播放
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3.4
  5. // @description 长按右方向键倍速播放,松开恢复原速,按 + 键增加倍速,按 - 键减少倍速,单击右方向键快进5秒。 按 ] 键从 1.5 倍速开始,每按一次增加 0.5 倍速,按 [ 键每次减少 0.5 倍速,按P键恢复1.0倍速。适配大部分网页播放器,尤其适配jellyfin等播放器播放nas内容。
  6. // @license MIT
  7. // @author diyun
  8. // @include http://*/*
  9. // @include https://*/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=gf.qytechs.cn
  11. // @grant none
  12. // ==/UserScript==
  13. (function () {
  14. "use strict";
  15. let currentUrl = location.href;
  16. let keydownListener = null;
  17. let keyupListener = null;
  18. let urlObserver = null;
  19. let videoObserver = null; // 添加此行
  20. let videoChangeObserver = null;
  21. let activeObservers = new Set();
  22. // 完整的清理函数
  23. function cleanup() {
  24. // 清理所有事件监听器
  25. if (keydownListener) {
  26. document.removeEventListener("keydown", keydownListener, true);
  27. keydownListener = null;
  28. }
  29. if (keyupListener) {
  30. document.removeEventListener("keyup", keyupListener, true);
  31. keyupListener = null;
  32. }
  33. // 清理所有观察器
  34. activeObservers.forEach((observer) => {
  35. if (observer && observer.disconnect) {
  36. observer.disconnect();
  37. }
  38. });
  39. activeObservers.clear();
  40. videoObserver = null;
  41. urlObserver = null;
  42. videoChangeObserver = null;
  43. }
  44. // 等待视频元素加载
  45. function waitForVideoElement() {
  46. return new Promise((resolve, reject) => {
  47. const maxAttempts = 10;
  48. let attempts = 0;
  49. const checkVideo = () => {
  50. const video = document.querySelector("video");
  51. if (video && video.readyState >= 1) {
  52. return video;
  53. }
  54. return null;
  55. };
  56. // 立即检查
  57. const video = checkVideo();
  58. if (video) {
  59. resolve(video);
  60. return;
  61. }
  62. // 创建观察器
  63. const observer = new MutationObserver(() => {
  64. attempts++;
  65. const video = checkVideo();
  66. if (video) {
  67. observer.disconnect();
  68. resolve(video);
  69. } else if (attempts >= maxAttempts) {
  70. observer.disconnect();
  71. console.warn("未找到视频元素,脚本已停止运行");
  72. reject({ type: "no_video" }); // 使用对象替代 Error
  73. }
  74. });
  75. observer.observe(document.body, {
  76. childList: true,
  77. subtree: true,
  78. });
  79. activeObservers.add(observer);
  80. // 设置超时
  81. setTimeout(() => {
  82. observer.disconnect();
  83. activeObservers.delete(observer);
  84. console.warn("等待视频元素超时,脚本已停止运行");
  85. reject({ type: "timeout" }); // 使用对象替代 Error
  86. }, 10000);
  87. });
  88. }
  89. // 显示浮动提示
  90. function showFloatingMessage(message) {
  91. // 添加样式
  92. const style = document.createElement('style');
  93. style.textContent = `
  94. .floating-message {
  95. position: fixed;
  96. top: 10%;
  97. left: 50%;
  98. transform: translateX(-50%);
  99. background: rgba(0, 0, 0, 0.8);
  100. color: white;
  101. padding: 8px 16px;
  102. border-radius: 4px;
  103. z-index: 2147483647;
  104. pointer-events: none;
  105. font-size: 1.1em;
  106. text-align: center;
  107. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  108. transition: opacity 0.3s ease;
  109. }
  110. `;
  111. document.head.appendChild(style);
  112.  
  113. // 清除已存在的提示
  114. const existingMessages = document.querySelectorAll('.floating-message');
  115. existingMessages.forEach(el => el.remove());
  116.  
  117. // 创建并显示新提示
  118. const messageEl = document.createElement('div');
  119. messageEl.className = 'floating-message';
  120. messageEl.textContent = message;
  121.  
  122. // 获取全屏元素或回退到body
  123. const fullscreenElement = document.fullscreenElement ||
  124. document.webkitFullscreenElement ||
  125. document.mozFullScreenElement ||
  126. document.msFullscreenElement;
  127. const targetContainer = fullscreenElement || document.body;
  128. targetContainer.appendChild(messageEl);
  129.  
  130. // 2秒后自动移除
  131. setTimeout(() => {
  132. if (messageEl.parentElement) {
  133. messageEl.remove();
  134. }
  135. }, 2000);
  136. }
  137. // 检查是否在可输入元素中
  138. function isInInputElement(event) {
  139. const target = event.target;
  140. // 检查元素是否是输入类型
  141. if (target.tagName === 'INPUT' ||
  142. target.tagName === 'TEXTAREA' ||
  143. target.tagName === 'SELECT' ||
  144. target.isContentEditable) {
  145. return true;
  146. }
  147. // 检查元素是否在编辑器中 (常见的编辑器包含这些类名或ID)
  148. const editorElements = ['editor', 'ace_editor', 'monaco-editor', 'CodeMirror'];
  149. for (const className of editorElements) {
  150. if (target.closest(`.${className}`) || target.closest(`#${className}`)) {
  151. return true;
  152. }
  153. }
  154. return false;
  155. }
  156. // 初始化脚本
  157. async function init() {
  158. cleanup();
  159. try {
  160. const video = await waitForVideoElement();
  161. console.log("找到视频元素:", video);
  162. const key = "ArrowRight"; // 监听的按键
  163. const increaseKey = "Equal"; // + 键
  164. const decreaseKey = "Minus"; // - 键
  165. const quickIncreaseKey = "BracketRight"; // 】键
  166. const quickDecreaseKey = "BracketLeft"; // 【键
  167. const resetSpeedKey = "KeyP"; // P键
  168. let targetRate = 2; // 目标倍速
  169. let currentQuickRate = 1.0; // 当前快速倍速
  170. let keyDownTime = 0; // 添加按键开始时间记录
  171. let originalRate = video.playbackRate; // 保存原始播放速度
  172. let isSpeedUp = false; // 添加一个标记来跟踪是否处于加速状态
  173. // 监听视频元素变化
  174. if (video.parentElement) {
  175. videoChangeObserver = new MutationObserver((mutations) => {
  176. const hasVideoChanges = mutations.some(
  177. (mutation) =>
  178. Array.from(mutation.removedNodes).some(
  179. (node) => node.tagName === "VIDEO"
  180. ) ||
  181. Array.from(mutation.addedNodes).some(
  182. (node) => node.tagName === "VIDEO"
  183. )
  184. );
  185. if (hasVideoChanges) {
  186. console.log("视频元素变化,重新初始化");
  187. cleanup();
  188. init().catch(console.error);
  189. }
  190. });
  191. videoChangeObserver.observe(video.parentElement, {
  192. childList: true,
  193. subtree: true,
  194. });
  195. activeObservers.add(videoChangeObserver);
  196. }
  197. // 创建新的事件监听器
  198. keydownListener = (e) => {
  199. // 首先检查是否在输入元素中,如果是则不处理快捷键
  200. if (isInInputElement(e)) {
  201. return;
  202. }
  203. if (e.code === key) {
  204. e.preventDefault();
  205. e.stopImmediatePropagation();
  206. // 记录按下时间
  207. if (!keyDownTime) {
  208. keyDownTime = Date.now();
  209. }
  210. // 如果按下超过300ms,认为是长按,进入加速模式
  211. if (!isSpeedUp && Date.now() - keyDownTime > 300) {
  212. isSpeedUp = true;
  213. originalRate = video.playbackRate;
  214. video.playbackRate = targetRate;
  215. showFloatingMessage(`开始 ${targetRate} 倍速播放`);
  216. }
  217. }
  218. // 按】键增加当前播放倍速
  219. if (e.code === quickIncreaseKey) {
  220. e.preventDefault();
  221. e.stopImmediatePropagation();
  222. if (currentQuickRate === 1.0) {
  223. currentQuickRate = 1.5;
  224. } else {
  225. currentQuickRate += 0.5;
  226. }
  227. video.playbackRate = currentQuickRate;
  228. showFloatingMessage(`当前播放速度:${currentQuickRate}x`);
  229. }
  230. // 按【键减少当前播放倍速
  231. if (e.code === quickDecreaseKey) {
  232. e.preventDefault();
  233. e.stopImmediatePropagation();
  234. if (currentQuickRate > 0.5) {
  235. currentQuickRate -= 0.5;
  236. video.playbackRate = currentQuickRate;
  237. showFloatingMessage(`当前播放速度:${currentQuickRate}x`);
  238. }
  239. }
  240. // 按P键恢复1.0倍速
  241. if (e.code === resetSpeedKey || e.key.toLowerCase() === "p") {
  242. e.preventDefault();
  243. e.stopImmediatePropagation();
  244. currentQuickRate = 1.0;
  245. video.playbackRate = 1.0;
  246. showFloatingMessage("恢复正常播放速度");
  247. }
  248. // 按 + 键:增加 targetRate 的值
  249. if (e.code === increaseKey) {
  250. e.preventDefault();
  251. e.stopImmediatePropagation();
  252. targetRate += 0.5;
  253. showFloatingMessage(`下次倍速:${targetRate}`);
  254. }
  255. // 按 - 键:减少 targetRate 的值
  256. if (e.code === decreaseKey) {
  257. e.preventDefault();
  258. e.stopImmediatePropagation();
  259. if (targetRate > 0.5) {
  260. targetRate -= 0.5;
  261. showFloatingMessage(`下次倍速:${targetRate}`);
  262. } else {
  263. showFloatingMessage("倍速已达到最小值 0.5");
  264. }
  265. }
  266. };
  267. keyupListener = (e) => {
  268. // 首先检查是否在输入元素中,如果是则不处理快捷键
  269. if (isInInputElement(e)) {
  270. return;
  271. }
  272. if (e.code === key) {
  273. e.preventDefault();
  274. e.stopImmediatePropagation();
  275. const pressTime = Date.now() - keyDownTime;
  276. // 如果按下时间小于300ms,认为是点击,快进5秒
  277. if (pressTime < 300) {
  278. video.currentTime += 5;
  279. }
  280. // 如果处于加速状态,恢复原速
  281. if (isSpeedUp) {
  282. video.playbackRate = originalRate;
  283. showFloatingMessage(`恢复 ${originalRate} 倍速播放`);
  284. isSpeedUp = false;
  285. }
  286. // 重置状态
  287. keyDownTime = 0;
  288. }
  289. };
  290. // 绑定事件监听器
  291. document.addEventListener("keydown", keydownListener, true);
  292. document.addEventListener("keyup", keyupListener, true);
  293. return true;
  294. } catch (error) {
  295. console.error("初始化失败:", error);
  296. return false;
  297. }
  298. }
  299. // 监听 URL 变化
  300. function watchUrlChange() {
  301. urlObserver = new MutationObserver(() => {
  302. if (location.href !== currentUrl) {
  303. currentUrl = location.href;
  304. console.log("URL变化,重新初始化");
  305. cleanup();
  306. setTimeout(() => init().catch(console.error), 1000);
  307. }
  308. });
  309. urlObserver.observe(document.body, {
  310. childList: true,
  311. subtree: true,
  312. });
  313. activeObservers.add(urlObserver);
  314. // 增强的 History API 监听
  315. const handleStateChange = () => {
  316. if (location.href !== currentUrl) {
  317. currentUrl = location.href;
  318. cleanup();
  319. setTimeout(() => init().catch(console.error), 1000);
  320. }
  321. };
  322. const originalPushState = history.pushState;
  323. const originalReplaceState = history.replaceState;
  324. history.pushState = function () {
  325. originalPushState.apply(this, arguments);
  326. handleStateChange();
  327. };
  328. history.replaceState = function () {
  329. originalReplaceState.apply(this, arguments);
  330. handleStateChange();
  331. };
  332. window.addEventListener("popstate", handleStateChange);
  333. }
  334. // 启动脚本
  335. const startScript = async () => {
  336. let retryCount = 0;
  337. const maxRetries = 3;
  338. const tryInit = async () => {
  339. try {
  340. const success = await init();
  341. if (success) {
  342. watchUrlChange();
  343. } else if (retryCount < maxRetries) {
  344. retryCount++;
  345. console.warn(`初始化重试 (${retryCount}/${maxRetries})`); // 改为警告
  346. setTimeout(tryInit, 2000);
  347. }
  348. } catch (error) {
  349. // 检查错误类型
  350. if (error && (error.type === "no_video" || error.type === "timeout")) {
  351. return; // 直接返回,不做额外处理
  352. }
  353. console.warn("启动失败:", error);
  354. if (retryCount < maxRetries) {
  355. retryCount++;
  356. setTimeout(tryInit, 2000);
  357. }
  358. }
  359. };
  360. tryInit();
  361. };
  362. startScript();
  363. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址