// ==UserScript== // @name 虎牙-根除"主播优选"悬浮广告(源头阻断版) // @namespace huya-zbyx-ad // @version 3.3.0 // @description 精准拦截虎牙"主播优选"(电商直播弹窗)悬浮广告:只按小程序 uuid 拦广告,不误伤上电视/赛事助手等正常功能;同时清除广点通视频广告及其占位框,彻底修复广告被拦后画面被挤小、右侧留白/马赛克;页面隐藏时自动休眠,不卡弹幕。 // @description:zh-CN 精准拦截虎牙"主播优选"(电商直播弹窗)悬浮广告:只按小程序 uuid 拦广告,不误伤上电视/赛事助手等正常功能;同时清除广点通视频广告及其占位框,彻底修复广告被拦后画面被挤小、右侧留白/马赛克;页面隐藏时自动休眠,不卡弹幕。 // @description:en Precisely blocks Huya's floating "主播优选" ad popup by mini-app uuid, without breaking normal mini-apps. Also removes GDT video ads and their placeholder boxes, fixing the shrunken-video / right-side blank issue. Idles when the tab is hidden. // @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== (function () { 'use strict'; /* ============ 可调配置 ============ */ const CFG = { // 精准打击:只拦这些虎牙小程序。改版后如冒出新广告,把它的 uuid 加进来即可。 // 如何拿到 uuid:F12 看被拦 iframe 的域名,形如 xxxxxxxx-.ext.huya.com blockExtUuids: [ '7q5dta52' // 「电商直播弹窗」——就是"主播优选"悬浮广告 ], blockExtNames: [ '电商直播弹窗', '主播优选' ], killGdtVideo: true, // 广点通(gdt)广告视频:静音并快进到底 domSweep: true, // 兜底:漏出来的"主播优选"卡片摘除 log: false // 控制台打印拦截日志 }; /* ================================= */ const say = (...a) => { if (CFG.log) console.log('[虎牙去广告]', ...a); }; // ext 弹窗域名形如: -.ext.huya.com const EXT_RE = /^(?:https?:)?\/\/([a-z0-9]+)-([a-z0-9]+)\.ext\.huya\.com/i; function blockedExt(u) { const s = String(u || ''); const m = s.match(EXT_RE); if (!m) return false; const uuid = m[2]; if (CFG.blockExtUuids.indexOf(uuid) !== -1) return true; // 名字匹配:/des/.../h5/index_xxx.html 前的目录里带名字时也能命中 for (const n of CFG.blockExtNames) if (s.indexOf(encodeURIComponent(n)) !== -1) return true; return false; } const isAdVideo = (u) => /adsmind\.gdtimg\.com|ads_svp_video|pgdt\.gtimg\.cn|gdt\.qq\.com/i.test(String(u || '')); const isAdImg = (u) => /adsmind\.gdtimg\.com|pgdt\.gtimg\.cn|gdt\.qq\.com|adsview|huyaad|ad\.huya/i.test(String(u || '')); /* ---------- 1.5 让虎牙"自己"认为装了广告拦截器 ---------- 虎牙 vplayerUI.js 里有 detectAdBlockerByImg():先插一张广告图片探针, 图片加载失败(onerror)就判定为"用户装了拦截插件",于是走它自己的关闭分支 (reportExternalEvent + 收起广告),而不是弹广告。 我们让广告域名的图片必然加载失败,从而借用虎牙自身的清理逻辑。 */ try { localStorage.setItem('adBlock', '1'); } catch (e) {} const IMG_D = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src'); Object.defineProperty(HTMLImageElement.prototype, 'src', { configurable: true, get() { return IMG_D.get.call(this); }, set(v) { if (isAdImg(v)) { say('阻断广告图片探针:', String(v).slice(0, 90)); return; } return IMG_D.set.call(this, v); } }); /* ---------- 1.6 CSS:广告容器一律不占布局 ---------- 这些类名来自虎牙 vplayerUI.js 的广告模板(ext-ad / video-ad / banner-ad / publicscreen-ad / reward-ad / end-ad / component-ab-pannel)。 广告被拦后虎牙会留下 ab-default-bg("加载中…") 占位框,画面就是这样被挤小的。 */ const AD_HIDE_CSS = [ '.ext-ab-wrap', '.ext-ab-content', '#ext-ab', '.ext-ab-close', '.video-ab-warp', '.banner-ab-warp', '.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-wrap', '.new-reward-ab-container', '.end-ab-container', '.end-ab-banner', '.component-ab-pannel', '#public-screen-ab', '#ab-banner', '.player-live-recommend-card', '.recommend-ab-full-mode', '.ab-fixed-style', '.ab-icon', '.ab-close-btn', '.ab-logo', '.ab-title', '.ab-qr-card', '.ab-skip', '.ab-time' ].join(',') + '{display:none!important;visibility:hidden!important;pointer-events:none!important;width:0!important;height:0!important;min-width:0!important;min-height:0!important;margin:0!important;padding:0!important;overflow:hidden!important;}'; // 关键例外:虎牙插播广告时会把直播 video(#hy-video) 暂时搬进广告容器, // 上面这条 display:none 会把直播画面一起藏掉 → 黑屏。必须强制保持可见。 const AD_HIDE_EXEMPT = '.ab-content video, .ab-container video, .ext-ab-content video, ' + '.ab-main video, #ext-ab video, #pre-ab-video video, .video-ab-warp video, ' + '.banner-ab-warp video, #public-screen-ab video' + '{display:block!important;visibility:visible!important;opacity:1!important;}'; function injectCSS(css, id) { if (!css || document.getElementById(id)) return; const s = document.createElement('style'); s.id = id; s.textContent = css; (document.head || document.documentElement).appendChild(s); } /* ---------- 1. 只拦广告小程序的 iframe(其余小程序正常放行) ---------- */ const D = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'src'); Object.defineProperty(HTMLIFrameElement.prototype, 'src', { configurable: true, get() { return D.get.call(this); }, set(v) { if (blockedExt(v)) { say('拦截广告小程序 iframe:', String(v).slice(0, 100)); pokeLayout(); return; } return D.set.call(this, v); } }); const SA = Element.prototype.setAttribute; Element.prototype.setAttribute = function (k, v) { if (this.tagName === 'IFRAME' && k === 'src' && blockedExt(v)) { say('拦截广告小程序 iframe(setAttribute):', String(v).slice(0, 100)); pokeLayout(); return; } return SA.call(this, k, v); }; // 广告被拦后,虎牙播放器可能已经把画面缩小、右侧留了广告位。 // 广播 resize 让播放器按"无广告"重新计算布局,恢复全屏。 const pokeLayout = (() => { let t = null; return () => { if (t) return; t = setTimeout(() => { t = null; try { window.dispatchEvent(new Event('resize')); } catch (e) {} say('已通知播放器重新计算布局'); }, 150); }; })(); /* ---------- 2. 广点通广告视频:让广告媒体加载失败,虎牙秒跳广告 ---------- */ // 之前"静音+快进到底"会让虎牙的插播流程跑完整个生命周期: // 它先冻结直播画面(黑屏)→播广告→收到 ended 才恢复直播,中间黑屏好几秒。 // 现在直接不让广告视频加载,虎牙 startPlay 报错后立刻走恢复分支。 { const VD = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'src'); Object.defineProperty(HTMLMediaElement.prototype, 'src', { configurable: true, get() { return VD.get.call(this); }, set(v) { if (isAdVideo(v)) { say('阻断广告视频加载:', String(v).slice(0, 90)); const el = this; setTimeout(() => { try { el.dispatchEvent(new Event('error')); } catch (e) {} }, 0); return; } return VD.set.call(this, v); } }); } if (CFG.killGdtVideo) { const handle = (v) => { if (!v || v.__adHandled) return; if (!isAdVideo(v.currentSrc || v.src)) return; v.__adHandled = true; say('广告视频,静音并快进'); try { v.muted = true; v.volume = 0; } catch (e) {} const skip = () => { if (!v.duration || !isFinite(v.duration)) return; try { v.currentTime = Math.max(v.duration - 0.1, v.currentTime); } catch (e) {} }; // 广告被拦后虎牙会留一个 ab-default-bg("加载中…") 占位框继续占布局, // 必须把整条广告外壳摘掉,画面才会恢复全屏。 const AD_SHELL = /(^|[\s_-])(ab-|ext-ab|public-screen-ab|new-reward-ab|end-ab|player-live-recommend|component-ab)/i; const cleanup = () => { let box = v; for (let i = 0; i < 6 && box && box.parentElement; i++) { const c = (typeof box.className === 'string') ? box.className : ''; if (AD_SHELL.test(c)) break; box = box.parentElement; } const c = box && (typeof box.className === 'string') ? box.className : ''; if (box && box !== document.body && AD_SHELL.test(c)) { // 生死攸关:虎牙插播广告时会把直播 video(#hy-video) 搬进广告容器里, // 直接 remove() 会连直播画面一起删掉 → 彻底黑屏。先把 video 抢回来。 try { const pv = document.getElementById('player-video') || document.querySelector('.player-video'); box.querySelectorAll('video').forEach((vv) => { if (pv && vv !== document.getElementById('hy-video')) return; say('抢救直播 video 出广告容器'); if (pv) pv.appendChild(vv); }); } catch (e) {} say('移除广告外壳:', c.slice(0, 60)); box.remove(); } pokeLayout(); }; v.addEventListener('ended', cleanup, true); v.addEventListener('loadedmetadata', skip, true); v.addEventListener('durationchange', skip, true); v.addEventListener('canplay', skip, true); // 广告视频一出现就清场,不等它播完(否则占位框会一直挤着画面) setTimeout(cleanup, 300); const t = setInterval(() => { if (!document.contains(v)) { clearInterval(t); pokeLayout(); return; } skip(); cleanup(); }, 1000); skip(); cleanup(); }; const scan = () => { try { document.querySelectorAll('video').forEach(handle); } catch (e) {} }; document.addEventListener('DOMContentLoaded', scan, true); window.addEventListener('load', scan, true); setInterval(scan, 4000); } /* ---------- 3. DOM 兜底(轻量:只在可见时跑,不做全量子树扫描) ---------- */ function sweep() { if (!CFG.domSweep) return; injectCSS(AD_HIDE_CSS, 'hy-ad-hide-css'); injectCSS(AD_HIDE_EXEMPT, 'hy-ad-hide-exempt'); // 3.0 虎牙留下的"加载中…"广告占位框(广告被拦后仍占布局,画面就是这样被挤小的) try { document.querySelectorAll('.ab-default-bg').forEach((el) => { let box = el; for (let i = 0; i < 5 && box.parentElement; i++) { box = box.parentElement; const c = (typeof box.className === 'string') ? box.className : ''; if (/(^|[\s_-])(ab-|ext-ab|public-screen-ab|new-reward-ab|end-ab)/i.test(c)) break; } const c = (typeof box.className === 'string') ? box.className : ''; if (box !== document.body && /(^|[\s_-])(ab-|ext-ab|public-screen-ab)/i.test(c)) { try { const pv = document.getElementById('player-video') || document.querySelector('.player-video'); box.querySelectorAll('video').forEach((vv) => { if (pv) pv.appendChild(vv); }); } catch (e) {} say('移除广告占位框'); box.remove(); pokeLayout(); } }); } catch (e) {} // 3.1 残留的广告小程序 iframe try { document.querySelectorAll('iframe').forEach((f) => { if (blockedExt(f.src) || blockedExt(f.getAttribute('src') || '')) { say('移除残留广告 iframe'); f.remove(); } }); } catch (e) {} // 3.2 直播画面若被压小(右侧留了广告位),通知播放器重新计算 try { document.querySelectorAll('video').forEach((v) => { if (!v.clientWidth || !v.videoWidth) return; const box = v.parentElement; if (!box) return; const bw = box.getBoundingClientRect().width; if (bw > 0 && v.clientWidth < bw * 0.95) pokeLayout(); }); } catch (e) {} // 3.3 文案兜底:只查小标签,命中才回溯。刻意保持轻量,避免拖慢页面。 try { const nodes = document.querySelectorAll('.ab-title, .ab-name, .ad-title, span, p'); for (let i = 0; i < nodes.length; i++) { const el = nodes[i]; if (el.children.length) continue; // 只看叶子节点 const t = (el.textContent || '').trim(); if (t !== '主播优选') continue; const r = el.getBoundingClientRect(); if (!r.width || !r.height) continue; let card = el; for (let j = 0; j < 8 && card.parentElement; j++) { const rr = card.getBoundingClientRect(); if (rr.width >= 200 && rr.height >= 180) break; card = card.parentElement; } const rr = card.getBoundingClientRect(); if (rr.width >= 200 && rr.height >= 180) { say('移除主播优选卡片'); card.remove(); } } } catch (e) {} } /* ---------- 3.5 切后台回来的"回归广告"秒关 ---------- 虎牙对切后台回来的用户会主动插一次广告弹窗(源码: visibilitychange 计时入队), 插播时先冻结直播画面(黑屏),再等广告探针流程走完才恢复。 这里在回到前台后的 10 秒窗口内:发现广告壳就直接点它自己的关闭按钮, 直播画面被暂停就立刻续播,把黑屏窗口压到最短。 */ let visibleAt = 0; document.addEventListener('visibilitychange', () => { if (!document.hidden) visibleAt = Date.now(); }, true); function resumeKick() { const inWindow = () => visibleAt && (Date.now() - visibleAt < 10000); const kick = () => { if (!inWindow()) return; // 1) 广告壳出现了就点它自己的关闭/跳过按钮 try { document.querySelectorAll( '#pre-ab-video, .ext-ab-wrap, .video-ab-warp, .banner-ab-warp, #public-screen-ab, .new-reward-ab-wrap' ).forEach((shell) => { const r = shell.getBoundingClientRect(); if (!r.width || !r.height) return; const btn = shell.querySelector('.ext-ab-close, .ab-close-btn, .close-btn, .ab-skip, .btn-hotzone'); if (btn) { say('自动关闭回归广告'); btn.click(); } }); } catch (e) {} // 2) 直播画面被冻结(暂停)就续播 try { const v = document.getElementById('hy-video'); if (v && v.paused && !v.ended) { say('恢复直播画面续播'); const pp = v.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch (e) {} }; setTimeout(kick, 400); setTimeout(kick, 1200); setTimeout(kick, 2500); setTimeout(kick, 5000); } document.addEventListener('visibilitychange', () => { if (!document.hidden) resumeKick(); }, true); /* ---------- 3.6 像素级黑屏自愈 ---------- 虎牙插播广告(前台也会触发)会冻结直播画面: 视频还在解码(有声音)但画面全黑。 这里直接从视频帧取样亮度, 连续两次全黑就: ① 先踢一脚 play/resize; ② 仍黑则重选当前清晰度(等效软刷新流, 不丢弹幕不刷新页面)。 */ (function blackGuard() { const cv = document.createElement('canvas'); cv.width = 64; cv.height = 36; const ctx = cv.getContext('2d', { willReadFrequently: true }); let blackStreak = 0; let probing = false; const kickOnce = (() => { let done = false; return () => { if (done) return; done = true; try { const v = document.getElementById('hy-video'); if (v) { const pp = v.play(); if (pp && pp.catch) pp.catch(() => {}); } window.dispatchEvent(new Event('resize')); } catch (e) {} setTimeout(() => { done = false; }, 3000); }; })(); const reselectQuality = () => { try { // 点当前"on"的清晰度项, 触发虎牙重拉流(软刷新) const cur = document.querySelector('#player-ctrl-wrap .player-videotype-list li.on') || document.querySelector('#player-ctrl-wrap .player-videotype-list li'); if (cur) { say('黑屏自愈: 重选清晰度刷新流'); cur.click(); } } catch (e) {} }; setInterval(() => { if (document.hidden || probing) return; const v = document.getElementById('hy-video'); if (!v || v.paused || !v.videoWidth) { blackStreak = 0; return; } probing = true; try { ctx.drawImage(v, 0, 0, 64, 36); const d = ctx.getImageData(0, 0, 64, 36).data; let sum = 0; for (let i = 0; i < d.length; i += 4) sum += (d[i] + d[i + 1] + d[i + 2]) / 3; const luma = sum / (d.length / 4); if (luma < 3) { blackStreak++; say('检测到黑屏帧 luma=' + luma.toFixed(1) + ' streak=' + blackStreak); if (blackStreak === 2) kickOnce(); else if (blackStreak >= 4) { blackStreak = 0; reselectQuality(); } } else { blackStreak = 0; } } catch (e) { /* 画布被污染等情况, 跳过 */ } probing = false; }, 2000); })(); /* ---------- 3.7 直播 video 归位守卫 ---------- 虎牙插播广告会把 #hy-video 搬进广告层再搬回, 中途若被我们的清理误删或藏住就会黑屏。 这里每秒确认一次: 直播 video 必须待在 #player-video 里且可见, 否则立刻归位。 */ setInterval(() => { if (document.hidden) return; try { const v = document.getElementById('hy-video'); if (!v) return; const pv = document.getElementById('player-video') || document.querySelector('.player-video'); if (!pv) return; const holder = v.parentElement; const misplaced = holder !== pv && !pv.contains(v); const cs = getComputedStyle(v); const invisible = cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0'; if (misplaced || invisible) { say('直播 video 归位 (misplaced=' + misplaced + ' invisible=' + invisible + ')'); if (!pv.contains(v)) pv.appendChild(v); v.style.setProperty('display', 'block', 'important'); v.style.setProperty('visibility', 'visible', 'important'); v.style.setProperty('opacity', '1', 'important'); v.style.setProperty('width', '100%', 'important'); v.style.setProperty('height', '100%', 'important'); if (v.paused) { const pp = v.play(); if (pp && pp.catch) pp.catch(() => {}); } } } catch (e) {} }, 1000); /* ---------- 4. 调度:页面可见才工作,切后台完全停摆 ---------- */ let timer = null; let mo = null; function stop() { if (timer) { clearInterval(timer); timer = null; } if (mo) { mo.disconnect(); mo = null; } } function start() { if (timer) return; sweep(); timer = setInterval(sweep, 4000); mo = new MutationObserver(() => { // 节流:合并成每轮一次,避免高频回调堆积 if (mo.__pending) return; mo.__pending = true; setTimeout(() => { mo.__pending = false; sweep(); }, 500); }); try { mo.observe(document.body || document.documentElement, { childList: true, subtree: true }); } catch (e) {} } document.addEventListener('visibilitychange', () => { if (document.hidden) stop(); else start(); }, true); const boot = () => { if (document.hidden) stop(); else start(); }; if (document.body || document.documentElement) boot(); else document.addEventListener('readystatechange', function f() { if (document.documentElement) { document.removeEventListener('readystatechange', f); boot(); } }, true); })();