// ==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();
});
})();