Skip to content

Instantly share code, notes, and snippets.

@lunamoth
Last active June 9, 2025 14:41
Show Gist options
  • Save lunamoth/0a448d97bc94029911ba2e101ee61d00 to your computer and use it in GitHub Desktop.
Save lunamoth/0a448d97bc94029911ba2e101ee61d00 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name 유튜브 볼륨 부스터
// @namespace http://tampermonkey.net/
// @version 3.7
// @description 화면 오른쪽 하단에 표시되는 컨트롤러를 통해 유튜브의 작은 소리를 100% 이상으로 증폭시켜주는 스크립트입니다. 전체화면에서는 자동으로 숨겨집니다.
// @author Gemini & lunamoth
// @match *://*.youtube.com/*
// @grant GM_addStyle
// @run-at document-body
// ==/UserScript==
(function() {
'use strict';
const config = {
MIN_VOLUME: 100,
MAX_VOLUME: 500,
VOLUME_STEP: 10,
WARNING_THRESHOLD: 300,
COLOR_NORMAL: '#0a84ff',
COLOR_WARNING: '#ff9500',
get VOLUME_RANGE() { return this.MAX_VOLUME - this.MIN_VOLUME; }
};
const DOM = {
CONTAINER_ID: 'volumeBoosterGlobalContainer',
TOGGLE_BUTTON_ID: 'boosterToggleButton',
PANEL_ID: 'volumeBoosterPanel',
SLIDER_ID: 'volumeBoosterSlider',
CLASSES: {
HEADER: 'booster-header',
TITLE: 'booster-title',
HEADER_ACTIONS: 'booster-header-actions',
BTN: 'booster-btn',
RESET_BTN: 'booster-reset-btn',
CLOSE_BTN: 'booster-close-btn',
CONTROLS: 'booster-controls',
DISPLAY: 'booster-display',
BOOST_ACTIVE: 'boost-active',
}
};
GM_addStyle(`
#${DOM.CONTAINER_ID} { position: fixed; bottom: 75px; right: 22px; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Roboto", sans-serif; display: none; }
#${DOM.TOGGLE_BUTTON_ID}, #${DOM.PANEL_ID} { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(20px) saturate(1.8); -webkit-backdrop-filter: blur(20px) saturate(1.8); border: 1px solid rgba(255, 255, 255, 0.5); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
#${DOM.TOGGLE_BUTTON_ID} { width: 32px; height: 32px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: #333; transition: transform 0.2s, box-shadow 0.2s, color 0.2s; }
#${DOM.TOGGLE_BUTTON_ID}:hover { transform: scale(1.1); }
#${DOM.TOGGLE_BUTTON_ID}.${DOM.CLASSES.BOOST_ACTIVE} { color: ${config.COLOR_NORMAL}; }
#${DOM.PANEL_ID} { display: none; width: 220px; padding: 12px 16px; border-radius: 16px; }
.${DOM.CLASSES.HEADER} { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.${DOM.CLASSES.TITLE} { font-size: 15px; font-weight: 600; color: #111; }
.${DOM.CLASSES.HEADER_ACTIONS} { display: flex; align-items: center; gap: 4px; }
.${DOM.CLASSES.BTN} { border: none; background: transparent; cursor: pointer; transition: background-color 0.2s; display: flex; align-items: center; justify-content: center; }
.${DOM.CLASSES.RESET_BTN} { padding: 4px; border-radius: 6px; font-size: 12px; font-weight: 500; color: ${config.COLOR_NORMAL}; }
.${DOM.CLASSES.RESET_BTN}:hover { background-color: rgba(10, 132, 255, 0.1); }
.${DOM.CLASSES.CLOSE_BTN} { width: 24px; height: 24px; border-radius: 50%; font-size: 12px; color: rgba(0,0,0,0.4); }
.${DOM.CLASSES.CLOSE_BTN}:hover { background-color: rgba(0,0,0,0.08); }
.${DOM.CLASSES.CONTROLS} { display: flex; align-items: center; }
.${DOM.CLASSES.DISPLAY} { min-width: 45px; margin-left: 14px; font-family: "SF Mono", "Menlo", monospace; font-size: 14px; font-weight: 500; text-align: right; color: #555; }
#${DOM.SLIDER_ID} { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; background: rgba(0,0,0,0.1); border-radius: 3px; outline: none; cursor: pointer; transition: background 0.2s; }
#${DOM.SLIDER_ID}::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; background: #fff; border-radius: 50%; border: 1px solid rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
`);
const state = {
volume: config.MIN_VOLUME,
isPanelVisible: false,
isUiVisible: false
};
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const processedVideos = new WeakMap();
let uiElements = null;
let currentGainNode = null;
let currentSourceNode = null;
const render = () => {
if (!uiElements) return;
const { container, panel, toggleButton, slider, display } = uiElements;
const { volume, isPanelVisible, isUiVisible } = state;
container.style.display = isUiVisible ? 'block' : 'none';
panel.style.display = isPanelVisible ? 'block' : 'none';
toggleButton.style.display = isPanelVisible ? 'none' : 'flex';
toggleButton.classList.toggle(DOM.CLASSES.BOOST_ACTIVE, volume > config.MIN_VOLUME);
slider.value = volume;
display.textContent = `${volume}%`;
const percentage = (volume - config.MIN_VOLUME) * 100 / config.VOLUME_RANGE;
const fillColor = volume >= config.WARNING_THRESHOLD ? config.COLOR_WARNING : config.COLOR_NORMAL;
slider.style.background = `linear-gradient(to right, ${fillColor} ${percentage}%, rgba(0,0,0,0.1) ${percentage}%)`;
};
const setState = (newState) => {
Object.assign(state, newState);
render();
};
const handleVolumeChange = (newVolume) => {
const volume = parseInt(newVolume, 10);
if (currentGainNode) {
currentGainNode.gain.setValueAtTime(volume / 100, audioContext.currentTime);
}
setState({ volume });
};
const createAudioProcessor = (video) => {
if (processedVideos.has(video)) return;
if (currentSourceNode) currentSourceNode.disconnect();
if (currentGainNode) currentGainNode.disconnect();
try {
const source = audioContext.createMediaElementSource(video);
const gainNode = audioContext.createGain();
source.connect(gainNode).connect(audioContext.destination);
currentSourceNode = source;
currentGainNode = gainNode;
processedVideos.set(video, true);
handleVolumeChange(state.volume);
} catch (error) {
setState({ isUiVisible: false });
}
};
const manageUiAndAudio = () => {
const video = document.querySelector('video');
if (!video) {
setState({ isUiVisible: false });
return;
}
// [수정됨] 전체화면 상태가 아닐 때만 UI를 표시하도록 조건 추가
const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
if (!isFullscreen) {
setState({ isUiVisible: true });
}
createAudioProcessor(video);
};
// [추가됨] 전체화면 변경을 감지하는 핸들러
const handleFullscreenChange = () => {
const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
if (isFullscreen) {
// 전체화면 진입 시 UI 숨김
setState({ isUiVisible: false });
} else {
// 전체화면 해제 시 UI 상태를 다시 확인하여 표시
manageUiAndAudio();
}
};
const createElement = (tag, options = {}, children = []) => {
const element = document.createElement(tag);
Object.assign(element, options);
if (options.className) {
const classes = Array.isArray(options.className) ? options.className : [options.className];
element.classList.add(...classes);
}
children.forEach(child => element.appendChild(child));
return element;
};
const createUI = () => {
const display = createElement('span', { className: DOM.CLASSES.DISPLAY });
const slider = createElement('input', {
type: 'range',
id: DOM.SLIDER_ID,
min: config.MIN_VOLUME,
max: config.MAX_VOLUME,
step: config.VOLUME_STEP,
oninput: (e) => handleVolumeChange(e.target.value)
});
const controls = createElement('div', { className: DOM.CLASSES.CONTROLS }, [slider, display]);
const resetButton = createElement('button', {
className: [DOM.CLASSES.BTN, DOM.CLASSES.RESET_BTN],
textContent: 'Reset',
onclick: () => handleVolumeChange(config.MIN_VOLUME)
});
const closeButton = createElement('button', {
className: [DOM.CLASSES.BTN, DOM.CLASSES.CLOSE_BTN],
textContent: '✖',
onclick: () => setState({ isPanelVisible: false })
});
const headerActions = createElement('div', { className: DOM.CLASSES.HEADER_ACTIONS }, [resetButton, closeButton]);
const title = createElement('div', { className: DOM.CLASSES.TITLE, textContent: 'Volume Boost' });
const header = createElement('div', { className: DOM.CLASSES.HEADER }, [title, headerActions]);
const panel = createElement('div', { id: DOM.PANEL_ID }, [header, controls]);
const toggleButton = createElement('button', {
id: DOM.TOGGLE_BUTTON_ID,
textContent: '🔊',
onclick: () => setState({ isPanelVisible: true })
});
const container = createElement('div', { id: DOM.CONTAINER_ID }, [toggleButton, panel]);
document.body.appendChild(container);
return { container, toggleButton, panel, slider, display };
};
const cleanup = () => {
if (uiElements && uiElements.container) uiElements.container.remove();
if (currentSourceNode) currentSourceNode.disconnect();
if (currentGainNode) currentGainNode.disconnect();
document.removeEventListener('yt-navigate-finish', initialize);
window.removeEventListener('beforeunload', cleanup);
// [수정됨] 추가된 이벤트 리스너 제거
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
};
const initialize = () => {
if (audioContext.state === 'suspended') {
audioContext.resume();
}
if (!uiElements) {
uiElements = createUI();
}
manageUiAndAudio();
render();
if (!window.volumeBoosterInitialized) {
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange); // for Safari/Chrome
window.volumeBoosterInitialized = true;
}
};
document.addEventListener('yt-navigate-finish', initialize);
window.addEventListener('beforeunload', cleanup);
if (document.body) {
initialize();
} else {
document.addEventListener('DOMContentLoaded', initialize);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment