// ==UserScript==
// @name Chzzk 올인원 스크립트 (Auto Quality + Ad Popup Removal + Unmute)
// @namespace http://tampermonkey.net/
// @version 3.6.8
// @description Chzzk 방송에서 자동 화질 설정, 광고 팝업 차단, 음소거 자동 해제, 스크롤 잠금 해제
// @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==
(function () {
const originalRemoveChild = Node.prototype.removeChild;
Node.prototype.removeChild = function (child) {
if (!child || child.parentNode !== this) return child;
return originalRemoveChild.call(this, child);
};
})();
(async () => {
"use strict";
const APPLY_COOLDOWN = 1000;
const CONFIG = {
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",
},
};
const common = {
regex: {
adBlockDetect: /광고\s*차단\s*프로그램.*사용\s*중/i,
},
async: {
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
waitFor: (selector, timeout = CONFIG.defaultTimeout) => {
const effective = Math.max(timeout, CONFIG.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);
});
},
},
text: {
clean: (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;
},
},
dom: {
remove: (el) => el?.remove(),
clearStyle: (el) => el?.removeAttribute("style"),
},
log: {
DEBUG: true,
info: (...args) => common.log.DEBUG && console.log(...args),
success: (...args) => common.log.DEBUG && console.log(...args),
warn: (...args) => common.log.DEBUG && console.warn(...args),
error: (...args) => common.log.DEBUG && console.error(...args),
groupCollapsed: (...args) =>
common.log.DEBUG && console.groupCollapsed(...args),
table: (...args) => common.log.DEBUG && console.table(...args),
groupEnd: (...args) => common.log.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 TOGGLE_CLASS = "chzzk-helper-toggle";
async function addHeaderMenu() {
const toolbar = await common.async.waitFor('.toolbar_section__IPbBC');
if (!toolbar || toolbar.querySelector('.allinone-settings-wrapper')) return;
const wrapper = document.createElement('div');
wrapper.className = 'toolbar_item__Kbygr allinone-settings-wrapper';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'toolbar_item__Kbygr allinone-settings-button';
btn.innerHTML = `
올인원 환경설정
`;
wrapper.appendChild(btn);
const profileItem = toolbar.querySelector('.toolbar_profile__k50kI');
if (profileItem) toolbar.insertBefore(wrapper, profileItem);
else toolbar.appendChild(wrapper);
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: '10px',
top: 'calc(100% + 7px)',
width: '240px',
zIndex: 13000,
});
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: CONFIG.storageKeys.autoUnmute,
svg: unmuteSvgOff,
onSvg: unmuteSvgOn,
label: '자동 언뮤트'
},
{
key: CONFIG.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', () => {
GM.getValue(item.key, false).then(active =>
GM.setValue(item.key, !active).then(() => {
const newActive = !active;
itemBtn.style.opacity = newActive ? '1' : '0.4';
itemBtn.querySelector('svg').outerHTML = newActive ? item.onSvg : item.svg;
itemBtn.querySelector('.state-text').textContent = newActive ? 'ON' : 'OFF';
location.reload();
})
);
});
}
menu.appendChild(itemBtn);
});
document.body.appendChild(menu);
btn.addEventListener('click', e => {
e.stopPropagation();
const rect = btn.getBoundingClientRect();
menu.style.top = `${rect.bottom + 4}px`;
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;
window.toggleDebugLogs = async () => {
const key = CONFIG.storageKeys.debugLog;
const current = await GM.getValue(key, false);
const next = !current;
await GM.setValue(key, next);
common.log.DEBUG = next;
console.log(`🛠️ Debug logs ${next ? 'ENABLED' : 'DISABLED'}`);
};
window.toggleDebugLogs = async () => {
const key = CONFIG.storageKeys.debugLog;
const current = await GM.getValue(key, false);
const next = !current;
await GM.setValue(key, next);
common.log.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 = common.text.extractResolution(raw);
if (res) {
await GM.setValue(CONFIG.storageKeys.quality, res);
common.log.groupCollapsed(
"%c💾 [Quality] 수동 화질 저장됨",
CONFIG.styles.success
);
common.log.table([{
"선택 해상도": res,
원본: common.text.clean(raw)
},]);
common.log.groupEnd();
}
}, {
capture: true
}
);
},
async getPreferred() {
const stored = await GM.getValue(CONFIG.storageKeys.quality, 1080);
return parseInt(stored, 10);
},
async applyPreferred() {
const now = Date.now();
if (this._applying || now - this._lastApply < APPLY_COOLDOWN) return;
this._applying = true;
this._lastApply = now;
const target = await this.getPreferred();
let cleaned = "(선택 실패)",
pick = null;
try {
const btn = await common.async.waitFor(CONFIG.selectors.qualityBtn);
btn.click();
const menu = await common.async.waitFor(CONFIG.selectors.qualityMenu);
menu.click();
await common.async.sleep(CONFIG.minTimeout);
const items = Array.from(
document.querySelectorAll(CONFIG.selectors.qualityItems)
);
pick =
items.find(
(i) => common.text.extractResolution(i.textContent) === target
) ||
items.find((i) => /\d+p/.test(i.textContent)) ||
items[0];
cleaned = pick ? common.text.clean(pick.textContent) : cleaned;
if (pick)
pick.dispatchEvent(new KeyboardEvent("keydown", {
key: "Enter"
}));
else common.log.warn("[Quality] 화질 항목을 찾지 못함");
} catch (e) {
common.log.error(`[Quality] 선택 실패: ${e.message}`);
}
common.log.groupCollapsed(
"%c⚙️ [Quality] 자동 화질 적용",
CONFIG.styles.info
);
common.log.table([{
"대상 해상도": target
}]);
common.log.table([{
"선택 화질": cleaned,
"선택 방식": pick ? "자동" : "없음"
},]);
common.log.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(), CONFIG.minTimeout);
}
} catch (e) {
common.log.error(`[XHR] JSON 파싱 오류: ${e.message}`);
}
}
});
}
return oSend.call(this, body);
};
common.log.info("[XHR] live-detail 요청 감시 시작");
},
trackURLChange() {
let lastUrl = location.href;
let lastId = null;
const getId = (url) => (url.match(/live\/([\w-]+)/) ?? [])[1] || null;
const onUrlChange = () => {
const currentUrl = location.href;
if (currentUrl === lastUrl) return;
lastUrl = currentUrl;
const id = getId(currentUrl);
if (!id) {
common.log.info("[URLChange] 방송 ID 없음");
} else if (id !== lastId) {
lastId = id;
setTimeout(() => {
quality.applyPreferred();
injectSharpnessScript();
}, CONFIG.minTimeout);
} else {
common.log.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(node);
let vid = null;
if (node.tagName === "VIDEO") vid = node;
else if (node.querySelector?.("video"))
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();
common.log.success(
"%c▶️ [AutoPlay] 재생 성공",
CONFIG.styles.info
);
} catch (e) {
common.log.error(`⚠️ [AutoPlay] 재생 실패: ${e.message}`);
}
})();
}
}
}
if (document.body.style.overflow === "hidden") {
common.dom.clearStyle(document.body);
common.log.info("[BodyStyle] overflow:hidden 제거됨");
}
});
mo.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style"],
});
common.log.info("[Observer] 통합 감시 시작");
},
async unmuteAll(video) {
const autoUnmute = await GM.getValue(CONFIG.storageKeys.autoUnmute, true);
if (!autoUnmute) return common.log.info("[Unmute] 설정에 따라 스킵");
if (video.muted) {
video.muted = false;
common.log.success("[Unmute] video.muted 해제");
}
const btn = document.querySelector(
'button.pzp-pc-volume-button[aria-label*="음소거 해제"]'
);
if (btn) {
btn.click();
common.log.success("[Unmute] 버튼 클릭");
}
},
async tryRemoveAdPopup(node) {
try {
const txt = node.innerText || "";
if (common.regex.adBlockDetect.test(txt)) {
const cont = node.closest(CONFIG.selectors.popup) || node;
cont.remove();
common.dom.clearStyle(document.body);
common.log.groupCollapsed(
"%c✅ [AdPopup] 제거 성공",
CONFIG.styles.success
);
common.log.table([{
"제거된 텍스트": txt.slice(0, 100),
클래스: cont.className
},]);
common.log.groupEnd();
}
} catch (e) {
common.log.error(`[AdPopup] 제거 실패: ${e.message}`);
}
},
};
async function checkAndFixLowQuality(video) {
if (!video || video.__checkedAlready) return;
video.__checkedAlready = true;
await common.async.sleep(CONFIG.defaultTimeout);
let height = video.videoHeight || 0;
if (height === 0) {
await common.async.sleep(1000);
height = video.videoHeight || 0;
}
if (height === 0) {
return;
}
if (height <= 360) {
const preferred = await quality.getPreferred();
if (preferred !== height) {
common.log.warn(
`[QualityCheck] 저화질(${height}p) 감지, ${preferred}p로 복구`
);
await quality.applyPreferred();
} else {
common.log.warn(
"[QualityCheck] 현재 해상도가 사용자 선호값과 동일하여 복구 생략"
);
}
}
}
async function setDebugLogging() {
common.log.DEBUG = await GM.getValue(CONFIG.storageKeys.debugLog, false);
}
async function injectSharpnessScript() {
const enabled = await GM.getValue(
CONFIG.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);
common.log.success(
"%c[Sharpness] 외부 스크립트 삽입 완료",
CONFIG.styles.info
);
}
async function init() {
await setDebugLogging();
if (document.body.style.overflow === "hidden") {
common.dom.clearStyle(document.body);
common.log.success("[Init] overflow 잠금 해제");
}
if ((await GM.getValue(CONFIG.storageKeys.quality)) === undefined) {
await GM.setValue(CONFIG.storageKeys.quality, 1080);
common.log.success("[Init] 기본 화질 1080 저장");
}
if ((await GM.getValue(CONFIG.storageKeys.autoUnmute)) === undefined) {
await GM.setValue(CONFIG.storageKeys.autoUnmute, true);
common.log.success("[Init] 기본 언뮤트 ON 저장");
}
await addHeaderMenu();
common.observeElement(
CONFIG.selectors.headerMenu,
() => {
addHeaderMenu().catch(console.error);
},
false
);
await quality.applyPreferred();
await injectSharpnessScript();
}
function onDomReady() {
console.log("%c🔔 [ChzzkHelper] 스크립트 시작", CONFIG.styles.info);
quality.observeManualSelect();
observer.start();
init().catch(console.error);
}
handler.interceptXHR();
handler.trackURLChange();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", onDomReady);
} else {
onDomReady();
}
})();
(function () {
const skip = (t) => ["INPUT", "TEXTAREA"].includes(t.tagName) || t.isContentEditable;
const getBtn = () =>
document.querySelector(
'button[aria-label="넓은 화면"],button[aria-label="좁은 화면"]'
);
document.addEventListener(
"keydown",
(e) => {
if (skip(e.target) || e.ctrlKey || e.altKey || e.metaKey) return;
const v = document.querySelector("video");
if (!v) return;
const k = e.key.toLowerCase();
const actions = {
" ": () => (v.paused ? v.play() : v.pause()),
k: () => (v.paused ? v.play() : v.pause()),
m: () => (v.muted = !v.muted),
t: () => {
const b = getBtn();
b && b.click();
},
f: () =>
document.fullscreenElement ?
document.exitFullscreen() :
v.requestFullscreen && v.requestFullscreen(),
};
if (actions[k]) {
actions[k]();
e.preventDefault();
e.stopPropagation();
}
},
true
);
})();