Mobile Video Seek Gesture

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

// ==UserScript==
// @name         Mobile Video Seek Gesture
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  모바일 브라우저에서 좌우 스와이프 제스처로 동영상 탐색 및 길게 눌러 2배속 재생 (Shadow DOM 포함)
// @author       사용자
// @license      MIT
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let startX = 0;
    let initialTime = 0;
    let seeking = false;
    let timeChange = 0;
    let longPressTimeout = null; // 길게 누름 감지
    let isSpeedingUp = false; // 현재 배속 상태 확인
    let movedEnoughForSeek = false; // 스와이프 감지 여부
    let userPlaybackRates = new Map(); // 사용자 설정 배속 저장

    // 비디오별 오버레이 생성
    function createOverlay(video) {
        // 이미 오버레이가 있다면 제거
        if (video.overlay) video.overlay.remove();
        let overlay = document.createElement('div');
        overlay.style.position = 'absolute';
        overlay.style.top = '50%';
        overlay.style.left = '50%';
        overlay.style.transform = 'translate(-50%, -50%)';
        overlay.style.padding = '10px 20px';
        overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        overlay.style.color = '#fff';
        overlay.style.fontSize = '18px';
        overlay.style.textAlign = 'center';
        overlay.style.borderRadius = '8px';
        overlay.style.zIndex = '9999';
        overlay.style.display = 'none';
        overlay.style.lineHeight = '1.5'; // 줄 간격 설정
        video.parentElement.appendChild(overlay);
        video.overlay = overlay;  // 비디오에 오버레이 속성 추가
    }

    // 터치 시작 이벤트
    function onTouchStart(e, video) {
        if (!video) return;
        startX = e.touches[0].clientX;
        initialTime = video.currentTime;
        seeking = true;
        movedEnoughForSeek = false; // 초기화
        video.overlay.style.display = 'block';

        // 길게 누르면 배속 시작
        longPressTimeout = setTimeout(() => {
            if (!movedEnoughForSeek) { // 탐색 중이 아닐 때만 배속 적용
                userPlaybackRates.set(video, video.playbackRate); // 기존 배속 저장
                video.playbackRate = 2.0; // 2배속
                video.overlay.innerHTML = `<div>2x Speed</div>`;
                isSpeedingUp = true;
            }
        }, 500); // 0.5초 이상 누르면 배속
    }

    // 터치 이동 이벤트
    function onTouchMove(e, video) {
        if (!seeking || !video || isSpeedingUp) return;
        let deltaX = e.touches[0].clientX - startX;

        if (Math.abs(deltaX) > 10) { // 일정 거리 이상 움직이면 탐색 모드로 간주
            movedEnoughForSeek = true;
            clearTimeout(longPressTimeout); // 길게 누름 취소
        }

        timeChange = deltaX * 0.05; // 민감도 조정
        let newTime = initialTime + timeChange;
        // 비디오 길이를 넘지 않도록 시간 범위 제한
        newTime = Math.max(0, Math.min(newTime, video.duration));

        let timeChangeFormatted = formatTimeChange(timeChange);
        video.overlay.innerHTML = `
            <div>${formatCurrentTime(newTime)}</div>
            <div>(${timeChange >= 0 ? '+' : ''}${timeChangeFormatted})</div>
        `;
    }

    // 터치 종료 이벤트
    function onTouchEnd(video) {
        seeking = false;
        clearTimeout(longPressTimeout); // 길게 누름 감지 중단
        longPressTimeout = null; // longPressTimeout 초기화
        if (isSpeedingUp) {
            video.playbackRate = userPlaybackRates.get(video) || 1.0; // 원래 속도로 복귀
            isSpeedingUp = false;
        } else if (movedEnoughForSeek) {
            let newTime = initialTime + timeChange;
            // 비디오 길이를 넘지 않도록 시간 범위 제한
            newTime = Math.max(0, Math.min(newTime, video.duration));
            video.currentTime = newTime;
        }
        // 오버레이 숨기기 - 바로 숨겨짐 
        video.overlay.style.display = 'none';
        video.overlay.innerHTML = ''; // 이전에 표시된 내용도 비움
    }

    // 시간을 시:분:초 형식으로 변환
    function formatCurrentTime(seconds) {
        let absSeconds = Math.abs(seconds);
        let hours = Math.floor(absSeconds / 3600);
        let minutes = Math.floor((absSeconds % 3600) / 60);
        let secs = Math.floor(absSeconds % 60);
        if (hours > 0) {
            return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`;
        } else {
            return `${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`;
        }
    }

    // 시간 변화량을 형식화
    function formatTimeChange(seconds) {
        let sign = seconds < 0 ? '-' : ''; // 음수 표시
        let absSeconds = Math.abs(seconds);
        let hours = Math.floor(absSeconds / 3600);
        let minutes = Math.floor((absSeconds % 3600) / 60);
        let secs = Math.floor(absSeconds % 60);
        let fraction = Math.round((absSeconds % 1) * 100);
        if (absSeconds >= 3600) {
            return `${sign}${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`;
        } else if (absSeconds >= 60) {
            return `${sign}${minutes < 10 ? '0' : ''}${minutes}:${secs < 10 ? '0' : ''}${secs}`;
        } else {
            return `${sign}${secs < 10 ? '0' : ''}${secs}.${fraction < 10 ? '0' : ''}${fraction}`;
        }
    }

    // 비디오에 제스처 기능 추가 (내부 플래그 사용)
    function addGestureControls(video) {
        if (!video || video._gestureAdded) return;
        // 내부 플래그를 이용해 중복 추가 방지
        video._gestureAdded = true;
        createOverlay(video);

        // 배속을 초기화하고 사용자 설정 값으로 복원
        let userRate = userPlaybackRates.get(video) || 1.0;
        video.playbackRate = userRate; // 사용자 설정에 맞춰 배속 초기화

        // 사용자가 배속을 직접 변경했을 때 저장
        video.addEventListener('ratechange', () => {
            if (!isSpeedingUp) {
                userPlaybackRates.set(video, video.playbackRate);
            }
        });

        video.addEventListener('touchstart', (e) => onTouchStart(e, video));
        video.addEventListener('touchmove', (e) => onTouchMove(e, video));
        video.addEventListener('touchend', () => onTouchEnd(video));
    }

    // Shadow DOM 내 비디오 탐색
    function findVideosInShadow(root) {
        if (!root) return;
        let videos = root.querySelectorAll('video');
        videos.forEach(addGestureControls);
        root.querySelectorAll('*').forEach(el => {
            if (el.shadowRoot) findVideosInShadow(el.shadowRoot);
        });
    }

    // 모든 비디오에 제스처 추가
    function scanForVideos() {
        document.querySelectorAll('video').forEach(addGestureControls);
        document.querySelectorAll('*').forEach(el => {
            if (el.shadowRoot) findVideosInShadow(el.shadowRoot);
        });
    }

    // DOM 변경 감지 및 비디오 발견 시 제스처 추가
    const observer = new MutationObserver(scanForVideos);
    observer.observe(document.body, { childList: true, subtree: true });

    // 페이지 로딩 시 비디오 탐색
    window.addEventListener('load', scanForVideos);
})();

QingJ © 2025

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