// ==UserScript==
// @name Chzzk 올인원 스크립트 (Auto Quality + Ad Popup Removal + Unmute)
// @namespace http://tampermonkey.net/
// @version 4.1
// @description Chzzk 방송에서 자동 화질 설정, 광고 팝업 차단, 음소거 자동 해제, 스크롤 잠금 해제, 360p 복구
// @match https://chzzk.naver.com/*
// @icon https://chzzk.naver.com/favicon.ico
// @grant GM.getValue
// @grant GM.setValue
// @grant unsafeWindow
// @run-at document-start
// @license MIT
// @downloadURL none
// ==/UserScript==
(async () => {
"use strict";
class Config {
#applyCooldown = 1000;
#minTimeout = 500;
#defaultTimeout = 2000;
#storageKeys = {
quality: "chzzkPreferredQuality",
autoUnmute: "chzzkAutoUnmute",
debugLog: "chzzkDebugLog",
screenSharpness: "chzzkScreenSharp",
};
#selectors = {
popup: 'div[class^="popup_container"]',
qualityBtn: 'button[command="SettingCommands.Toggle"]',
qualityMenu: 'div[class*="pzp-pc-setting-intro-quality"]',
qualityItems: 'li.pzp-ui-setting-quality-item[role="menuitem"]',
headerMenu: ".header_service__DyG7M",
};
#styles = {
success: "font-weight:bold; color:green",
error: "font-weight:bold; color:red",
info: "font-weight:bold; color:skyblue",
warn: "font-weight:bold; color:orange",
};
#regex = {
adBlockDetect: /광고\s*차단\s*프로그램.*사용\s*중/i,
};
#debug = true;
get applyCooldown() { return this.#applyCooldown; }
get minTimeout() { return this.#minTimeout; }
get defaultTimeout() { return this.#defaultTimeout; }
get storageKeys() { return this.#storageKeys; }
get selectors() { return this.#selectors; }
get styles() { return this.#styles; }
get regex() { return this.#regex; }
get debug() { return this.#debug; }
set debug(value) { this.#debug = !!value; }
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
waitFor = (selector, timeout = this.#defaultTimeout) => {
const effective = Math.max(timeout, this.#minTimeout);
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const mo = new MutationObserver(() => {
const found = document.querySelector(selector);
if (found) {
mo.disconnect();
resolve(found);
}
});
mo.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
mo.disconnect();
reject(new Error("Timeout waiting for " + selector));
}, effective);
});
};
cleanText = (txt) => txt.trim().split(/\s+/).filter(Boolean).join(", ");
extractResolution = (txt) => {
const m = txt.match(/(\d{3,4})p/);
return m ? parseInt(m[1], 10) : null;
};
removeElement = (el) => el?.remove();
clearStyle = (el) => el?.removeAttribute("style");
info = (...args) => this.#debug && console.log(...args);
success = (...args) => this.#debug && console.log(...args);
warn = (...args) => this.#debug && console.warn(...args);
error = (...args) => this.#debug && console.error(...args);
groupCollapsed = (...args) => this.#debug && console.groupCollapsed(...args);
table = (...args) => this.#debug && console.table(...args);
groupEnd = (...args) => this.#debug && console.groupEnd(...args);
observeElement = (selector, callback, once = true) => {
const mo = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
callback(el);
if (once) mo.disconnect();
}
});
mo.observe(document.body, { childList: true, subtree: true });
const initial = document.querySelector(selector);
if (initial) {
callback(initial);
if (once) mo.disconnect();
}
};
}
const C = new Config();
async function addHeaderMenu() {
if (!document.getElementById('chzzk-allinone-styles')) {
const customStyles = document.createElement('style');
customStyles.id = 'chzzk-allinone-styles';
customStyles.textContent = `
.allinone-settings-button:hover {
background-color: var(--Surface-Interaction-Lighten-Hovered);
border-radius: 6px;
}
.button_label__fyHZ6 {
align-items: center;
background-color: var(--Surface-Neutral-Base);
border-radius: 6px;
box-shadow: 0 2px 2px var(--Shadow-Strong),0 2px 6px 2px var(--Shadow-Base);
color: var(--Content-Neutral-Cool-Stronger);
display: inline-flex;
font-family: -apple-system,BlinkMacSystemFont,Apple SD Gothic Neo,Helvetica,Arial,NanumGothic,나눔고딕,Malgun Gothic,맑은 고딕,Dotum,굴림,gulim,새굴림,noto sans,돋움,sans-serif;
font-size: 12px;
font-weight: 400;
height: 27px;
justify-content: center;
letter-spacing: -.3px;
line-height: 17px;
padding: 0 9px;
position: absolute;
white-space: nowrap;
z-index: 15000;
}
.allinone-tooltip-position {
top: calc(100% + 2px);
right: -10px;
}
`;
document.head.appendChild(customStyles);
}
const toolbar = await C.waitFor('.toolbar_section__maAwZ');
if (!toolbar || toolbar.querySelector('.allinone-settings-wrapper')) return;
const boxWrapper = document.createElement('div');
boxWrapper.className = 'toolbar_box__2DzCd';
const itemWrapper = document.createElement('div');
itemWrapper.className = 'toolbar_item__w9Z7l allinone-settings-wrapper';
itemWrapper.style.position = 'relative';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'button_container__ppWwB button_only_icon__kahz5 button_larger__4NrSP allinone-settings-button';
btn.innerHTML = `
올인원 환경설정
`;
btn.addEventListener('mouseenter', () => {
if (itemWrapper.querySelector('.button_label__fyHZ6')) return;
const tooltip = document.createElement('span');
tooltip.className = 'button_label__fyHZ6 allinone-tooltip-position';
tooltip.textContent = '올인원 환경설정';
itemWrapper.appendChild(tooltip);
});
btn.addEventListener('mouseleave', () => {
const tooltip = itemWrapper.querySelector('.button_label__fyHZ6');
if (tooltip) tooltip.remove();
});
itemWrapper.appendChild(btn);
boxWrapper.appendChild(itemWrapper);
const profileBox = toolbar.querySelector('.toolbar_profile_button__tZxIO')?.closest('.toolbar_box__2DzCd');
if (profileBox) {
toolbar.insertBefore(boxWrapper, profileBox);
} else {
toolbar.appendChild(boxWrapper);
}
const menu = document.createElement('div');
menu.className = 'allinone-settings-menu';
Object.assign(menu.style, {
position: 'absolute',
background: 'var(--color-bg-layer-02)',
borderRadius: '10px',
boxShadow: '0 8px 20px var(--color-shadow-layer01-02), 0 0 1px var(--color-shadow-layer01-01)',
color: 'var(--color-content-03)',
overflow: 'auto',
padding: '18px',
right: '0px',
top: 'calc(100% + 7px)',
width: '240px',
zIndex: 13000,
display: 'none'
});
itemWrapper.appendChild(menu);
const helpContent = document.createElement('div');
helpContent.className = 'allinone-help-content';
Object.assign(helpContent.style, {
display: 'none',
margin: '4px 0',
padding: '4px 8px 4px 34px',
fontFamily: 'Sandoll Nemony2, Apple SD Gothic NEO, Helvetica Neue, Helvetica, NanumGothic, Malgun Gothic, gulim, noto sans, Dotum, sans-serif',
fontSize: '14px',
color: 'var(--color-content-03)',
whiteSpace: 'pre-wrap',
});
helpContent.innerHTML =
'
메뉴 사용법 ' +
'' +
'1. 자동 언뮤트 ' +
'방송이 시작되면 자동으로 음소거를 해제합니다. 간헐적으로 음소거 상태로 전환되는 문제를 보완하기 위해 추가된 기능입니다.\n\n' +
'2. 선명한 화면 ' +
'“선명한 화면 2.0” 옵션을 활성화하면 개발자가 제작한 외부 스크립트를 적용하여, 기본 제공되는 선명도 기능을 대체합니다.' +
'
';
const helpBtn = document.createElement('button');
helpBtn.className = 'allinone-settings-item';
helpBtn.style.display = 'flex';
helpBtn.style.alignItems = 'center';
helpBtn.style.margin = '8px 0';
helpBtn.style.padding = '4px 8px';
helpBtn.style.fontFamily = 'Sandoll Nemony2, Apple SD Gothic NEO, Helvetica Neue, Helvetica, NanumGothic, Malgun Gothic, gulim, noto sans, Dotum, sans-serif';
helpBtn.style.fontSize = '14px';
helpBtn.style.color = 'inherit';
helpBtn.innerHTML = `
도움말
`;
helpBtn.addEventListener('click', () => {
helpContent.style.display = helpContent.style.display === 'none' ? 'block' : 'none';
});
menu.appendChild(helpBtn);
menu.appendChild(helpContent);
const unmuteSvgOff = ` `;
const unmuteSvgOn = ` `;
const sharpSvg = ` `;
const items = [
{ key: C.storageKeys.autoUnmute, svg: unmuteSvgOff, onSvg: unmuteSvgOn, label: '자동 언뮤트' },
{ key: C.storageKeys.screenSharpness, svg: sharpSvg, onSvg: sharpSvg, label: '선명한 화면 2.0' },
];
items.forEach(item => {
const itemBtn = document.createElement('button');
itemBtn.className = 'allinone-settings-item';
itemBtn.style.display = 'flex';
itemBtn.style.alignItems = 'center';
itemBtn.style.margin = '8px 0';
itemBtn.style.padding = '4px 8px';
itemBtn.style.fontFamily = 'Sandoll Nemony2, Apple SD Gothic NEO, Helvetica Neue, Helvetica, NanumGothic, Malgun Gothic, gulim, noto sans, Dotum, sans-serif';
itemBtn.style.fontSize = '14px';
itemBtn.style.color = 'inherit';
itemBtn.innerHTML = `
${item.svg}
${item.label}${item.key ? ' OFF ' : ''}
`;
if (!item.key) {
itemBtn.style.opacity = '1';
itemBtn.addEventListener('click', item.onClick);
} else {
GM.getValue(item.key, false).then(active => {
itemBtn.style.opacity = active ? '1' : '0.4';
if (active) itemBtn.querySelector('svg').outerHTML = item.onSvg;
const stateSpan = itemBtn.querySelector('.state-text');
stateSpan.textContent = active ? 'ON' : 'OFF';
});
itemBtn.addEventListener('click', async () => {
const active = await GM.getValue(item.key, false);
const newActive = !active;
await GM.setValue(item.key, newActive);
setTimeout(() => {
location.reload();
}, 100);
});
}
menu.appendChild(itemBtn);
});
btn.addEventListener('click', e => {
e.stopPropagation();
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
});
document.addEventListener('click', e => {
if (!menu.contains(e.target) && e.target !== btn) {
menu.style.display = 'none';
}
});
}
window.addHeaderMenu = addHeaderMenu;
unsafeWindow.toggleDebugLogs = async () => {
const key = C.storageKeys.debugLog;
const current = await GM.getValue(key, false);
const next = !current;
await GM.setValue(key, next);
C.debug = next;
console.log(`🛠️ Debug logs ${next ? 'ENABLED' : 'DISABLED'}`);
};
const quality = {
observeManualSelect() {
document.body.addEventListener("click", async (e) => {
const li = e.target.closest('li[class*="quality"]');
if (!li) return;
const raw = li.textContent;
const res = C.extractResolution(raw);
if (res) {
await GM.setValue(C.storageKeys.quality, res);
C.groupCollapsed("%c💾 [Quality] 수동 화질 저장됨", C.styles.success);
C.table([{ "선택 해상도": res, 원본: C.cleanText(raw) }]);
C.groupEnd();
}
}, { capture: true });
},
async getPreferred() {
const stored = await GM.getValue(C.storageKeys.quality, 1080);
return parseInt(stored, 10);
},
async applyPreferred() {
const now = Date.now();
if (this._applying || now - this._lastApply < C.applyCooldown) return;
this._applying = true;
this._lastApply = now;
const target = await this.getPreferred();
let cleaned = "(선택 실패)", pick = null;
try {
const btn = await C.waitFor(C.selectors.qualityBtn);
btn.click();
const menu = await C.waitFor(C.selectors.qualityMenu);
menu.click();
await C.sleep(C.minTimeout);
const items = Array.from(document.querySelectorAll(C.selectors.qualityItems));
pick =
items.find((i) => C.extractResolution(i.textContent) === target) ||
items.find((i) => /\d+p/.test(i.textContent)) ||
items[0];
cleaned = pick ? C.cleanText(pick.textContent) : cleaned;
if (pick) {
pick.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
} else {
C.warn("[Quality] 화질 항목을 찾지 못함");
}
} catch (e) {
C.error(`[Quality] 선택 실패: ${e.message}`);
}
C.groupCollapsed("%c⚙️ [Quality] 자동 화질 적용", C.styles.info);
C.table([{ "대상 해상도": target }]);
C.table([{ "선택 화질": cleaned, "선택 방식": pick ? "자동" : "없음" }]);
C.groupEnd();
this._applying = false;
},
};
const handler = {
interceptXHR() {
const oOpen = XMLHttpRequest.prototype.open;
const oSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (m, u, ...a) {
this._url = u;
return oOpen.call(this, m, u, ...a);
};
XMLHttpRequest.prototype.send = function (body) {
if (this._url?.includes("live-detail")) {
this.addEventListener("readystatechange", () => {
if (this.readyState === 4 && this.status === 200) {
try {
const data = JSON.parse(this.responseText);
if (data.content?.p2pQuality) {
data.content.p2pQuality = [];
const mod = JSON.stringify(data);
Object.defineProperty(this, "responseText", { value: mod });
Object.defineProperty(this, "response", { value: mod });
setTimeout(() => quality.applyPreferred(), C.minTimeout);
}
} catch (e) {
C.error(`[XHR] JSON 파싱 오류: ${e.message}`);
}
}
});
}
return oSend.call(this, body);
};
C.info("[XHR] live-detail 요청 감시 시작");
},
trackURLChange() {
let lastUrl = location.href;
let lastId = null;
const CHZZK_ID_REGEX = /(?:live|video)\/(?[^/]+)/;
const getId = (url) => (typeof url === 'string' ? (url.match(CHZZK_ID_REGEX)?.groups?.id || null) : null);
const onUrlChange = () => {
const currentUrl = location.href;
if (currentUrl === lastUrl) return;
lastUrl = currentUrl;
const id = getId(currentUrl);
if (!id) {
C.info("[URLChange] 방송 ID 없음");
} else if (id !== lastId) {
lastId = id;
setTimeout(() => {
quality.applyPreferred();
injectSharpnessScript();
}, C.minTimeout);
} else {
C.warn(`[URLChange] 같은 방송(${id}), 스킵`);
}
const svg = document.getElementById("sharpnessSVGContainer");
const style = document.getElementById("sharpnessStyle");
if (svg) svg.remove();
if (style) style.remove();
if (window.sharpness) {
window.sharpness.init();
window.sharpness.observeMenus();
}
};
["pushState", "replaceState"].forEach((method) => {
const original = history[method];
history[method] = function (...args) {
const result = original.apply(this, args);
window.dispatchEvent(new Event("locationchange"));
return result;
};
});
window.addEventListener("popstate", () =>
window.dispatchEvent(new Event("locationchange"))
);
window.addEventListener("locationchange", onUrlChange);
},
};
const observer = {
start() {
const mo = new MutationObserver((muts) => {
for (const mut of muts) {
for (const node of mut.addedNodes) {
if (node.nodeType !== 1) continue;
this.tryRemoveAdPopup();
let vid = null;
if (node.tagName === "VIDEO") {
vid = node;
} else if (node.querySelector) {
vid = node.querySelector("video");
}
if (/^\/live\/[^/]+/.test(location.pathname) && vid) {
this.unmuteAll(vid);
checkAndFixLowQuality(vid);
(async () => {
await new Promise((resolve) => {
const waitForReady = () => {
if (vid.readyState >= 4) return resolve();
setTimeout(waitForReady, 100);
};
waitForReady();
});
try {
await vid.play();
C.success("%c▶️ [AutoPlay] 재생 성공", C.styles.info);
} catch (e) {
C.error(`⚠️ [AutoPlay] 재생 실패: ${e.message}`);
}
})();
}
}
}
if (document.body.style.overflow === "hidden") {
const adPopup = Array.from(document.querySelectorAll(C.selectors.popup)).find(p => C.regex.adBlockDetect.test(p.textContent));
if (adPopup) {
C.clearStyle(document.body);
C.info("[BodyStyle] 광고 팝업으로 인한 overflow:hidden 제거됨");
}
}
});
mo.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style"],
});
C.info("[Observer] 통합 감시 시작");
},
async unmuteAll(video) {
const autoUnmute = await GM.getValue(C.storageKeys.autoUnmute, true);
if (!autoUnmute) return C.info("[Unmute] 설정에 따라 스킵");
if (video.muted) {
video.muted = false;
C.success("[Unmute] video.muted 해제");
}
const btn = document.querySelector('button.pzp-pc-volume-button[aria-label*="음소거 해제"]');
if (btn) {
btn.click();
C.success("[Unmute] 버튼 클릭");
}
},
async tryRemoveAdPopup() {
try {
const popups = document.querySelectorAll(C.selectors.popup);
for (const popup of popups) {
if (C.regex.adBlockDetect.test(popup.textContent)) {
const btn = popup.querySelector('button');
if (btn) {
const fk = Object.keys(btn).find(k =>
k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
);
if (fk) {
const props = btn[fk].memoizedProps || btn[fk].return.memoizedProps;
if (typeof props.exposureAdBlockPopup === 'function') {
props.exposureAdBlockPopup = () => { };
}
(props.confirmHandler || props.onClick || props.onClickHandler)?.({ isTrusted: true });
C.success("[AdPopup] React 광고 팝업 직접 닫기 실행");
return;
}
}
}
}
} catch (e) {
C.error(`[AdPopup] 자동 닫기 실패: ${e.message}`);
}
},
};
let isRecoveringQuality = false;
async function checkAndFixLowQuality(video) {
if (!video || video.__qualityMonitorAttached) return;
video.__qualityMonitorAttached = true;
C.info("[QualityCheck] 화질 모니터링 시작");
const performCheck = async () => {
if (video.paused || isRecoveringQuality) return;
const currentHeight = video.videoHeight;
if (currentHeight === 0) return;
const preferred = await quality.getPreferred();
if (currentHeight < preferred) {
C.warn(`[QualityCheck] 저화질(${currentHeight}p) 감지. 선호 화질(${preferred}p)로 복구 시도.`);
isRecoveringQuality = true;
await quality.applyPreferred();
setTimeout(() => {
isRecoveringQuality = false;
C.info("[QualityCheck] 화질 복구 쿨다운 종료.");
}, 120000);
}
};
video.addEventListener('loadedmetadata', performCheck);
setInterval(performCheck, 30000);
}
async function setDebugLogging() {
C.debug = await GM.getValue(C.storageKeys.debugLog, false);
}
async function injectSharpnessScript() {
const enabled = await GM.getValue(C.storageKeys.screenSharpness, false);
if (!enabled) return;
const script = document.createElement("script");
script.src = "https://update.greasyfork.icu/scripts/534918/Chzzk%20%EC%84%A0%EB%AA%85%ED%95%9C%20%ED%99%94%EB%A9%B4%20%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C.user.js";
script.async = true;
document.head.appendChild(script);
C.success("%c[Sharpness] 외부 스크립트 삽입 완료", C.styles.info);
}
async function init() {
await setDebugLogging();
if (document.body.style.overflow === "hidden") {
C.clearStyle(document.body);
C.success("[Init] overflow 잠금 해제");
}
if ((await GM.getValue(C.storageKeys.quality)) === undefined) {
await GM.setValue(C.storageKeys.quality, 1080);
C.success("[Init] 기본 화질 1080 저장");
}
if ((await GM.getValue(C.storageKeys.autoUnmute)) === undefined) {
await GM.setValue(C.storageKeys.autoUnmute, true);
C.success("[Init] 기본 언뮤트 ON 저장");
}
await addHeaderMenu();
C.observeElement(C.selectors.headerMenu, () => {
addHeaderMenu().catch(console.error);
}, false);
await quality.applyPreferred();
await injectSharpnessScript();
}
function onDomReady() {
console.log("%c🔔 [ChzzkHelper] 스크립트 시작", C.styles.info);
quality.observeManualSelect();
observer.start();
init().catch(console.error);
}
handler.interceptXHR();
handler.trackURLChange();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", onDomReady);
} else {
onDomReady();
}
})();