Mobile Video Seek Gesture

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

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