soop 방송 딜레이 자동 조정

soop 방송 딜레이 1.5초 이내로 자동 조정

// ==UserScript==
// @name         soop 방송 딜레이 자동 조정
// @namespace    https://greasyfork.org/ko/scripts/539405
// @version      1.3
// @description  soop 방송 딜레이 1.5초 이내로 자동 조정
// @icon         https://www.google.com/s2/favicons?sz=64&domain=www.sooplive.co.kr
// @author       다크초코
// @match        https://play.sooplive.co.kr/*
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        CHECK_INTERVAL: 100,        // 딜레이 체크 간격 (ms)
        HISTORY_DURATION: 2000,     // 평균 계산 기간 (ms) - 최근 2000ms동안 계산된 딜레이의 평균값을 기준으로 딜레이 조정
        TRIGGER_DELAY: 1500,        // 자동 조정 시작 딜레이 (ms) - 1500ms 도달시 조절 시작
        TARGET_DELAY: 1000,         // 목표 딜레이 (ms) - 1000ms까지 조정 후 정상 속도 복귀
        
        SPEED_LEVELS: [
            { minDelay: 5000, playbackRate: 1.3 }, // 5초 이상 - 1.3배
            { minDelay: 3000, playbackRate: 1.25 }, // 3초 이상 - 1.25배
            { minDelay: 2500, playbackRate: 1.2 }, // 2.5초 이상 - 1.2배    
            { minDelay: 2000, playbackRate: 1.15 }, // 2초 이상 - 1.15배
            { minDelay: 1500, playbackRate: 1.1 }, // 1.5초 이상 - 1.1배
            { minDelay: 0, playbackRate: 1.05 } // 그 외 - 1.05배
        ],
        NORMAL_RATE: 1.0            // 정상 재생 속도
    };

    let delayHistory = [];
    let isAdjusting = false;
    let checkInterval = null;
    let video = null;
    let currentPlaybackRate = 1.0;
    let fullscreenListenersAdded = false;
    let lastDisplayUpdate = 0;
    let cachedFullscreenState = false;
    let playbackRateProtection = false;
    let speedControlObserver = null;

    function findVideo() {
        return document.querySelector('video');
    }

    function calculateDelay(videoElement) {
        if (!videoElement) return null;
        
        try {
            const buffered = videoElement.buffered;
            if (buffered.length > 0) {
                const bufferedEnd = buffered.end(buffered.length - 1);
                const currentTime = videoElement.currentTime;
                const delay = bufferedEnd - currentTime;
                return delay >= 0 ? delay * 1000 : null;
            }
        } catch (error) {
            console.warn('딜레이 계산 오류:', error);
        }
        return null;
    }

    function addDelayToHistory(delay) {
        const now = Date.now();
        delayHistory.push({ delay, timestamp: now });
        
        delayHistory = delayHistory.filter(item => 
            now - item.timestamp <= CONFIG.HISTORY_DURATION
        );
    }

    function getAverageDelay() {
        if (delayHistory.length === 0) return 0;
        
        const totalDelay = delayHistory.reduce((sum, item) => sum + item.delay, 0);
        return totalDelay / delayHistory.length;
    }

    function getPlaybackRate(averageDelay) {
        for (const config of CONFIG.SPEED_LEVELS) {
            if (averageDelay >= config.minDelay) {
                return config.playbackRate;
            }
        }
        return CONFIG.SPEED_LEVELS[CONFIG.SPEED_LEVELS.length - 1].playbackRate;
    }

    function protectPlaybackRate() {
        if (!video || playbackRateProtection) return;
        
        playbackRateProtection = true;
        
        const originalPlaybackRateDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate');
        
        Object.defineProperty(video, 'playbackRate', {
            get: function() {
                return currentPlaybackRate;
            },
            set: function(value) {
                if (arguments.callee.caller && arguments.callee.caller.toString().includes('adjustPlaybackRate')) {
                    originalPlaybackRateDescriptor.set.call(this, value);
                    currentPlaybackRate = value;
                } else {
                    console.warn('외부 속도 제어 차단됨:', value);
                }
            },
            configurable: true
        });
        
        video.addEventListener('ratechange', function(e) {
            if (Math.abs(video.playbackRate - currentPlaybackRate) > 0.01) {
                e.preventDefault();
                e.stopPropagation();
                originalPlaybackRateDescriptor.set.call(video, currentPlaybackRate);
            }
        }, true);
    }

    function adjustPlaybackRate(rate) {
        if (!video) return;
        
        try {
            if (Math.abs(video.playbackRate - rate) > 0.01) {
                const originalSet = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate').set;
                originalSet.call(video, rate);
                currentPlaybackRate = rate;
            }
        } catch (error) {
            console.warn('재생 속도 조정 오류:', error);
        }
    }

    function updateFullscreenState() {
        cachedFullscreenState = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        return cachedFullscreenState;
    }

    function displayDelayInfo(currentDelay, averageDelay) {
        const now = Date.now();
        if (now - lastDisplayUpdate < 200) return;
        lastDisplayUpdate = now;

        try {
            let infoElement = document.getElementById('delay-info');
            if (!infoElement) {
                infoElement = document.createElement('div');
                infoElement.id = 'delay-info';
                infoElement.style.cssText = `
                    position: fixed;
                    bottom: 10px;
                    right: 10px;
                    background: rgba(0, 0, 0, 0.7);
                    color: white;
                    padding: 3px 5px;
                    border-radius: 3px;
                    font-family: monospace;
                    font-size: 7pt;
                    line-height: 1.2;
                    z-index: 10000;
                    opacity: 0.8;
                `;
                document.body.appendChild(infoElement);
            }

            infoElement.style.display = cachedFullscreenState ? 'none' : 'block';

            const status = isAdjusting ? `${currentPlaybackRate}x` : '1.0x';
            infoElement.textContent = `${averageDelay.toFixed(0)}ms ${status}`;
        } catch (error) {
            console.warn('모니터링창 업데이트 오류:', error);
        }
    }

    function handleFullscreenChange() {
        updateFullscreenState();
        
        try {
            const infoElement = document.getElementById('delay-info');
            if (infoElement) {
                infoElement.style.display = cachedFullscreenState ? 'none' : 'block';
            }
        } catch (error) {
            console.warn('전체화면 변경 처리 오류:', error);
        }
    }

    function setupFullscreenListener() {
        if (fullscreenListenersAdded) return;
        
        try {
            const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'];
            events.forEach(event => {
                document.addEventListener(event, handleFullscreenChange);
            });
            fullscreenListenersAdded = true;
            updateFullscreenState();
        } catch (error) {
            console.warn('전체화면 리스너 설정 오류:', error);
        }
    }

    function removeFullscreenListener() {
        if (!fullscreenListenersAdded) return;
        
        try {
            const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'];
            events.forEach(event => {
                document.removeEventListener(event, handleFullscreenChange);
            });
            fullscreenListenersAdded = false;
        } catch (error) {
            console.warn('전체화면 리스너 제거 오류:', error);
        }
    }

    function checkAndAdjustDelay() {
        if (!video) {
            video = findVideo();
            if (!video) return;
        }

        const currentDelay = calculateDelay(video);
        if (currentDelay === null) return;

        addDelayToHistory(currentDelay);

        const averageDelay = getAverageDelay();

        displayDelayInfo(currentDelay, averageDelay);

        if (delayHistory.length < 10) return;

        if (!isAdjusting && averageDelay >= CONFIG.TRIGGER_DELAY) {
            isAdjusting = true;
            const playbackRate = getPlaybackRate(averageDelay);
            adjustPlaybackRate(playbackRate);
        } else if (isAdjusting && averageDelay <= CONFIG.TARGET_DELAY) {
            isAdjusting = false;
            adjustPlaybackRate(CONFIG.NORMAL_RATE);
        } else if (isAdjusting) {
            const newPlaybackRate = getPlaybackRate(averageDelay);
            if (Math.abs(newPlaybackRate - currentPlaybackRate) > 0.01) {
                adjustPlaybackRate(newPlaybackRate);
            }
        }
    }

    function cleanup() {
        try {
            if (checkInterval) {
                clearInterval(checkInterval);
                checkInterval = null;
            }
            removeFullscreenListener();
            
            if (speedControlObserver) {
                speedControlObserver.disconnect();
                speedControlObserver = null;
            }
            
            const infoElement = document.getElementById('delay-info');
            if (infoElement && infoElement.parentNode) {
                infoElement.parentNode.removeChild(infoElement);
            }
        } catch (error) {
            console.warn('정리 작업 오류:', error);
        }
    }

    function resetState() {
        delayHistory = [];
        isAdjusting = false;
        video = null;
        currentPlaybackRate = 1.0;
        lastDisplayUpdate = 0;
        cachedFullscreenState = false;
        playbackRateProtection = false;
    }

    function startDelayAdjuster() {
        video = findVideo();
        
        if (!video) {
            setTimeout(startDelayAdjuster, 1000);
            return;
        }

        try {
            checkInterval = setInterval(checkAndAdjustDelay, CONFIG.CHECK_INTERVAL);
            setupFullscreenListener();
            protectPlaybackRate();
            disableKeyboardShortcuts();
            speedControlObserver = blockSpeedControlElements();
        } catch (error) {
            console.warn('딜레이 조정기 시작 오류:', error);
        }
    }

    function disableKeyboardShortcuts() {
        document.addEventListener('keydown', function(e) {
            if (e.target.tagName.toLowerCase() === 'input' || 
                e.target.tagName.toLowerCase() === 'textarea' ||
                e.target.contentEditable === 'true') {
                return;
            }
            
            if (e.key === '<' || e.key === '>' || 
                (e.shiftKey && (e.key === ',' || e.key === '.'))) {
                e.preventDefault();
                e.stopPropagation();
                console.warn('속도 제어 단축키 차단됨:', e.key);
            }
        }, true);
    }

    function blockSpeedControlElements() {
        const observer = new MutationObserver(() => {
            const speedControls = document.querySelectorAll('[class*="speed"], [class*="rate"], [id*="speed"], [id*="rate"]');
            speedControls.forEach(element => {
                if (element.tagName === 'BUTTON' || element.tagName === 'SELECT' || element.type === 'range') {
                    element.addEventListener('click', function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        console.warn('속도 제어 UI 차단됨');
                    }, true);
                }
            });
        });
        
        observer.observe(document.body, { childList: true, subtree: true });
        return observer;
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startDelayAdjuster);
    } else {
        startDelayAdjuster();
    }

    let currentUrl = location.href;
    const urlObserver = new MutationObserver(() => {
        if (location.href !== currentUrl) {
            currentUrl = location.href;
            
            cleanup();
            resetState();
            
            setTimeout(startDelayAdjuster, 1000);
        }
    });

    urlObserver.observe(document, { subtree: true, childList: true });

    window.addEventListener('beforeunload', () => {
        cleanup();
        urlObserver.disconnect();
    });

})();