// ==UserScript== // @name 虎牙-根除"主播优选"悬浮广告(源头阻断版) // @namespace huya-zbyx-ad // @version 4.0.0 // @description 拦截指定广告小程序,保护直播视频与画布,尝试原生关闭广告;不按亮度强制重播或切换清晰度。 // @author snowlove12 // @license MIT // @homepageURL https://scriptcat.org/ // @supportURL https://scriptcat.org/ // @match https://www.huya.com/* // @match https://hd.huya.com/* // @grant none // @run-at document-start // @noframes // @icon data:image/png;base64,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 // @tag 虎牙 // @tag 去广告 // @tag 直播 // ==/UserScript== (() => { 'use strict'; if (window.__huyaAdCleaner4) return; window.__huyaAdCleaner4 = true; const VERSION = '4.0.0'; const CFG = { blockExtUuids: ['7q5dta52'], blockExtNames: ['电商直播弹窗', '主播优选'], domSweep: true, killGdtVideo: true, // 只处理独立广告视频,绝不快进复用的直播 video。 log: false }; const HIDDEN = 'data-huya-ad-hidden', BLOCKED = 'data-huya-ad-blocked'; const MEDIA = 'video,canvas,audio'; const SHELLS = '.ext-ab-wrap,#ext-ab,.video-ab-warp,.banner-ab-warp,' + '#public-screen-ab,.new-reward-ab-wrap,#pre-ab-video,.end-ab-container'; const PARTS = '.ext-ab-content,.ab-container,.ab-content,.ab-content-container,' + '.ab-default-bg,.ab-qr-code-wrap,.ab-qr-code-container,.ab-middle,.ab-main,.ab-top,' + '.new-reward-ab-container,.end-ab-banner,.component-ab-pannel,#ab-banner,' + '.player-live-recommend-card,.recommend-ab-full-mode,.ab-qr-card'; const CLOSE = '.ext-ab-close,.ab-close-btn,.ab-skip'; const nativeSetAttribute = Element.prototype.setAttribute; const mediaState = new Map(), closeState = new WeakMap(); let pending = null, heartbeat = null, observer = null, started = false, lastResize = -Infinity; function record(action, detail = {}) { const entry = { version: VERSION, time: Date.now(), action, ...detail }; if (CFG.log) console.log('[虎牙去广告]', entry); document.dispatchEvent(new CustomEvent('huya-ad-action', { detail: JSON.stringify(entry) })); } function blockedExt(raw) { try { const url = new URL(String(raw), location.href); const match = url.hostname.match(/^([a-z0-9]+)-([a-z0-9]+)\.ext\.huya\.com$/i); if (!match || !/^https?:$/.test(url.protocol)) return false; if (CFG.blockExtUuids.includes(match[2].toLowerCase())) return true; let path = url.pathname; try { path = decodeURIComponent(path); } catch (_) {} return CFG.blockExtNames.some(name => path.includes(name)); } catch (_) { return false; } } function isAdVideo(raw) { try { const url = new URL(raw, location.href); return /^https?:$/.test(url.protocol) && ( /^(adsmind\.gdtimg\.com|pgdt\.gtimg\.cn|gdt\.qq\.com)$/i.test(url.hostname) || /(?:^|\/)ads_svp_video(?:\/|_)/i.test(url.pathname)); } catch (_) { return false; } } const hasMedia = el => el.matches(MEDIA) || !!el.querySelector(MEDIA); function schedule() { if (!started || document.hidden || pending !== null) return; pending = setTimeout(() => { pending = null; sweep(); }, 100); } // iframe 仍走真实 setter/load 生命周期,不改写图片或媒体原型。 const iframeSrc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'src'); function blockFrame(frame) { nativeSetAttribute.call(frame, BLOCKED, '1'); nativeSetAttribute.call(frame, HIDDEN, '1'); record('block-mini-app'); schedule(); } if (iframeSrc && iframeSrc.configurable && iframeSrc.set && iframeSrc.get) { Object.defineProperty(HTMLIFrameElement.prototype, 'src', { ...iframeSrc, set(value) { if (blockedExt(value)) { blockFrame(this); return iframeSrc.set.call(this, 'about:blank'); } this.removeAttribute(BLOCKED); this.removeAttribute(HIDDEN); return iframeSrc.set.call(this, value); } }); } Element.prototype.setAttribute = function (name, value) { if (this instanceof HTMLIFrameElement && String(name).toLowerCase() === 'src') { if (blockedExt(value)) { blockFrame(this); return nativeSetAttribute.call(this, name, 'about:blank'); } this.removeAttribute(BLOCKED); this.removeAttribute(HIDDEN); } return nativeSetAttribute.call(this, name, value); }; function hide(el) { if (!hasMedia(el)) { if (!el.hasAttribute(HIDDEN)) { nativeSetAttribute.call(el, HIDDEN, '1'); return true; } } else if (el.hasAttribute(HIDDEN)) { el.removeAttribute(HIDDEN); record('protect-media-container'); return true; } return false; } function closeAd(shell) { const button = shell.querySelector(CLOSE); if (!shell.isConnected || shell.hidden || shell.getAttribute('aria-hidden') === 'true' || shell.style.display === 'none') { if (button) closeState.delete(button); return; } if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return; const now = performance.now(); let state = closeState.get(button); if (!state) { state = { attempts: 0, time: -Infinity }; closeState.set(button, state); } // 有限重试以等待站点绑定事件,不点击通用热区或其他功能按钮。 if (state.attempts >= 3 || now - state.time < 1000) return; state.attempts++; state.time = now; record('close-ad-request', { shell: shell.id || shell.className, attempt: state.attempts }); button.click(); } function independentAd(video) { if (!CFG.killGdtVideo || video.ended || video.id === 'hy-video' || video.srcObject || video.closest('#player-video,.player-video')) return ''; const current = video.currentSrc, assigned = video.getAttribute('src'); if (!current || !isAdVideo(current)) return ''; // currentSrc 在换源途中可能仍是旧广告;此时不能再快进。 if (assigned && new URL(assigned, location.href).href !== current) return ''; const source = video.querySelector('source[src]'); if (!assigned && source && source.src !== current) return ''; return current; } function releaseMedia(video) { const state = mediaState.get(video); if (!state) return; if (video.muted === true) video.muted = state.muted; mediaState.delete(video); } function skipIndependentAd(video) { let src = ''; try { src = independentAd(video); } catch (_) {} const previous = mediaState.get(video); if (previous && previous.src !== src) releaseMedia(video); if (!src || !video.isConnected) { releaseMedia(video); return; } let state = mediaState.get(video); if (!state) { state = { src, muted: video.muted, sought: false }; mediaState.set(video, state); video.muted = true; } if (!state.sought && Number.isFinite(video.duration) && video.duration > 0 && video.readyState >= 1) { try { video.currentTime = video.duration; state.sought = true; record('skip-independent-ad'); } catch (_) { /* 等下一次真实媒体事件或扫描。 */ } } if (video.ended) releaseMedia(video); } function sweep() { if (document.hidden) return; let changed = false; if (CFG.domSweep) { document.querySelectorAll(SHELLS).forEach(closeAd); document.querySelectorAll(SHELLS + ',' + PARTS + ',[' + HIDDEN + ']').forEach(el => { changed = hide(el) || changed; }); } document.querySelectorAll('iframe').forEach(frame => { if (blockedExt(frame.getAttribute('src') || frame.src)) { blockFrame(frame); nativeSetAttribute.call(frame, 'src', 'about:blank'); changed = true; } }); for (const video of mediaState.keys()) if (!video.isConnected) releaseMedia(video); if (CFG.killGdtVideo) document.querySelectorAll('video').forEach(skipIndependentAd); if (changed && performance.now() - lastResize >= 1000) { lastResize = performance.now(); window.dispatchEvent(new Event('resize')); } } function suspend() { if (pending !== null) clearTimeout(pending); if (heartbeat !== null) clearInterval(heartbeat); pending = heartbeat = null; if (observer) observer.disconnect(); } function resume() { suspend(); if (!started || document.hidden) return; sweep(); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'class', 'style', 'hidden', 'aria-hidden', 'disabled'] }); heartbeat = setInterval(sweep, 1500); } function boot() { if (started || !document.documentElement) return; started = true; document.documentElement.setAttribute('data-huya-ad-state', JSON.stringify({ version: VERSION, ...CFG })); const css = document.createElement('style'); css.id = 'hy-ad-safe-css'; // :has() 同步保护新插入的媒体;不支持时宁可不隐藏,不冒险藏掉直播。 css.textContent = '@supports selector(:has(video)) { [' + HIDDEN + ']' + ':not(:is(video,canvas,audio)):not(:has(video,canvas,audio)) {display:none!important;} }'; (document.head || document.documentElement).appendChild(css); observer = new MutationObserver(schedule); for (const type of ['loadedmetadata', 'durationchange', 'canplay', 'ended', 'emptied', 'loadstart', 'error', 'abort']) { document.addEventListener(type, event => { if (!(event.target instanceof HTMLVideoElement)) return; if (['emptied', 'loadstart', 'ended', 'error', 'abort'].includes(type)) releaseMedia(event.target); else if (!document.hidden) skipIndependentAd(event.target); schedule(); }, true); } document.addEventListener('visibilitychange', resume); window.addEventListener('pagehide', suspend); window.addEventListener('pageshow', resume); record('started', { config: CFG }); resume(); } if (document.documentElement) boot(); else document.addEventListener('readystatechange', boot, { once: true }); })();