Mobile Video Seek Gesture

모바일 브라우저에서 좌우 스와이프 제스처로 동영상 탐색 및 길게 눌러 2배속 재생

  1. // ==UserScript==
  2. // @name Mobile Video Seek Gesture
  3. // @namespace http://tampermonkey.net/
  4. // @version 5.1
  5. // @description 모바일 브라우저에서 좌우 스와이프 제스처로 동영상 탐색 및 길게 눌러 2배속 재생
  6. // @author 사용자
  7. // @license MIT
  8. // @match *://*/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const userPlaybackRates = new Map(); // 사용자 설정 배속 저장
  16. let longPressTimeout = null; // 길게 누름 감지
  17.  
  18. // 제스처 공통 인터페이스 추상화
  19. function attachGesture(videoOrPlayer) {
  20. if (videoOrPlayer._gestureAdded) return;
  21. videoOrPlayer._gestureAdded = true;
  22.  
  23. const isNativeVideo = videoOrPlayer instanceof HTMLVideoElement;
  24. const isVideoJS = !isNativeVideo
  25. && typeof videoOrPlayer.currentTime === 'function'
  26. && typeof videoOrPlayer.playbackRate === 'function';
  27.  
  28. function getCurrentTime() {
  29. return isNativeVideo ? videoOrPlayer.currentTime : videoOrPlayer.currentTime();
  30. }
  31. function setCurrentTime(t) {
  32. if (isNativeVideo) videoOrPlayer.currentTime = t;
  33. else videoOrPlayer.currentTime(t);
  34. }
  35. function getDuration() {
  36. return isNativeVideo ? videoOrPlayer.duration : videoOrPlayer.duration();
  37. }
  38. function getPlaybackRate() {
  39. return isNativeVideo ? videoOrPlayer.playbackRate : videoOrPlayer.playbackRate();
  40. }
  41. function setPlaybackRate(rate) {
  42. if (isNativeVideo) videoOrPlayer.playbackRate = rate;
  43. else videoOrPlayer.playbackRate(rate);
  44. }
  45. function getContainer() {
  46. if (isNativeVideo) return videoOrPlayer.parentElement;
  47. else return videoOrPlayer.el();
  48. }
  49.  
  50. const container = getContainer();
  51.  
  52. // 오버레이 생성
  53. if (container.overlay) container.overlay.remove();
  54. const overlay = document.createElement('div');
  55. overlay.style.position = 'absolute';
  56. overlay.style.top = '50%';
  57. overlay.style.left = '50%';
  58. overlay.style.transform = 'translate(-50%, -50%)';
  59. overlay.style.padding = '10px 20px';
  60. overlay.style.backgroundColor = 'rgba(0,0,0,0.7)';
  61. overlay.style.color = '#fff';
  62. overlay.style.fontSize = '18px';
  63. overlay.style.textAlign = 'center';
  64. overlay.style.borderRadius = '8px';
  65. overlay.style.zIndex = '9999';
  66. overlay.style.display = 'none';
  67. overlay.style.lineHeight = '1.5'; // 줄 간격 설정
  68. container.appendChild(overlay);
  69. container.overlay = overlay; // 비디오에 오버레이 속성 추가
  70.  
  71. // 제스처 상태
  72. let startX = 0, initialTime = 0, seeking = false, timeChange = 0;
  73. let isSpeedingUp = false; // 현재 배속 상태 확인
  74. let movedEnoughForSeek = false; // 스와이프 감지 여부
  75.  
  76. // 시간 포맷
  77. // 시간을 시:분:초 형식으로 변환
  78. function formatCurrentTime(seconds) {
  79. let absSeconds = Math.floor(seconds); // 소수점 제거
  80. let hours = Math.floor(absSeconds / 3600);
  81. let minutes = Math.floor((absSeconds % 3600) / 60);
  82. let secs = absSeconds % 60;
  83.  
  84. if (hours > 0) {
  85. return `${hours < 10 ? '0' : ''}${hours}:` +
  86. `${minutes < 10 ? '0' : ''}${minutes}:` +
  87. `${secs < 10 ? '0' : ''}${secs}`;
  88. } else {
  89. return `${minutes < 10 ? '0' : ''}${minutes}:` +
  90. `${secs < 10 ? '0' : ''}${secs}`;
  91. }
  92. }
  93.  
  94. // 시간 변화량을 형식화
  95. function formatTimeChange(seconds) {
  96. const sign = seconds < 0 ? '-' : '+';
  97. let absSeconds = Math.floor(Math.abs(seconds));
  98. let hours = Math.floor(absSeconds / 3600);
  99. let minutes = Math.floor((absSeconds % 3600) / 60);
  100. let secs = absSeconds % 60;
  101.  
  102. if (hours > 0) {
  103. return `${sign}${hours < 10 ? '0' : ''}${hours}:` +
  104. `${minutes < 10 ? '0' : ''}${minutes}:` +
  105. `${secs < 10 ? '0' : ''}${secs}`;
  106. } else {
  107. return `${sign}${minutes < 10 ? '0' : ''}${minutes}:` +
  108. `${secs < 10 ? '0' : ''}${secs}`;
  109. }
  110. }
  111.  
  112. // 터치 시작 이벤트
  113. function onTouchStart(e) {
  114. startX = e.touches[0].clientX;
  115. initialTime = getCurrentTime();
  116. seeking = true;
  117. movedEnoughForSeek = false; // 초기화
  118. overlay.style.display = 'block';
  119.  
  120. // 길게 누르면 배속 시작
  121. // Video.js에서는 길게 누름 기능 적용 안함
  122. if (!isVideoJS) {
  123. longPressTimeout = setTimeout(() => {
  124. if (!movedEnoughForSeek) { // 탐색 중이 아닐 때만 배속 적용
  125. userPlaybackRates.set(videoOrPlayer, getPlaybackRate()); // 기존 배속 저장
  126. setPlaybackRate(2.0); // 2배속
  127. overlay.innerHTML = `<div>2x Speed</div>`;
  128. isSpeedingUp = true;
  129. }
  130. }, 500); // 0.5초 이상 누르면 배속
  131. }
  132. }
  133.  
  134. // 터치 이동 이벤트
  135. function onTouchMove(e) {
  136. if (!seeking || isSpeedingUp) return;
  137. const deltaX = e.touches[0].clientX - startX;
  138. if (Math.abs(deltaX) > 10) { // 일정 거리 이상 움직이면 탐색 모드로 간주
  139. movedEnoughForSeek = true;
  140. clearTimeout(longPressTimeout); // 길게 누름 취소
  141. }
  142. timeChange = deltaX * 0.05; // 민감도 조정
  143. let newTime = initialTime + timeChange;
  144. // 비디오 길이를 넘지 않도록 시간 범위 제한
  145. newTime = Math.max(0, Math.min(newTime, getDuration()));
  146. overlay.innerHTML = `
  147. <div>${formatCurrentTime(newTime)}</div>
  148. <div>(${formatTimeChange(timeChange)})</div>
  149. `;
  150. }
  151.  
  152. // 터치 종료 이벤트
  153. function onTouchEnd() {
  154. seeking = false;
  155. clearTimeout(longPressTimeout); // 길게 누름 감지 중단
  156. longPressTimeout = null; // longPressTimeout 초기화
  157.  
  158. if (isSpeedingUp) {
  159. setPlaybackRate(userPlaybackRates.get(videoOrPlayer) || 1.0); // 원래 속도로 복귀
  160. isSpeedingUp = false;
  161. } else if (movedEnoughForSeek) {
  162. let newTime = initialTime + timeChange;
  163. // 비디오 길이를 넘지 않도록 시간 범위 제한
  164. newTime = Math.max(0, Math.min(newTime, getDuration()));
  165. setCurrentTime(newTime);
  166. }
  167.  
  168. // 오버레이 숨기기 - 바로 숨겨짐
  169. overlay.style.display = 'none';
  170. overlay.innerHTML = ''; // 이전에 표시된 내용도 비움
  171. }
  172.  
  173. container.addEventListener('touchstart', onTouchStart);
  174. container.addEventListener('touchmove', onTouchMove);
  175. container.addEventListener('touchend', onTouchEnd);
  176.  
  177. if (isNativeVideo) {
  178. videoOrPlayer.addEventListener('ratechange', () => {
  179. if (!isSpeedingUp) userPlaybackRates.set(videoOrPlayer, videoOrPlayer.playbackRate);
  180. });
  181. }
  182.  
  183. if (isVideoJS) {
  184. videoOrPlayer.on('ratechange', () => {
  185. userPlaybackRates.set(videoOrPlayer, videoOrPlayer.playbackRate());
  186. });
  187. }
  188. }
  189.  
  190. // 재귀적 Shadow DOM 포함 video 탐색
  191. function findAllVideos(root = document) {
  192. const videos = Array.from(root.querySelectorAll('video'));
  193. root.querySelectorAll('*').forEach(el => {
  194. if (el.shadowRoot) videos.push(...findAllVideos(el.shadowRoot));
  195. });
  196. return videos;
  197. }
  198.  
  199. // 모든 비디오에 제스처 추가
  200. function scanVideos() {
  201. findAllVideos().forEach(v => attachGesture(v));
  202. if (typeof videojs !== 'undefined') {
  203. Object.values(videojs.getAllPlayers()).forEach(p => attachGesture(p));
  204. }
  205. }
  206.  
  207. // DOM 변경 감지 및 비디오 발견 시 제스처 추가
  208. const observer = new MutationObserver(scanVideos);
  209. observer.observe(document.body, { childList: true, subtree: true });
  210.  
  211. // 페이지 로딩 시 비디오 탐색
  212. window.addEventListener('load', scanVideos);
  213. })();

QingJ © 2025

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