不隐藏广告;在检测到广告时自动静音;当出现“跳过广告”按钮时自动点击,仅使用官方按钮,不再快进/重载视频。
Od
// ==UserScript==
// @name YouTube Ad Muter + Button Auto-Skip (VoidMuser)
// @namespace
// @version 2.0.0
// @description 不隐藏广告;在检测到广告时自动静音;当出现“跳过广告”按钮时自动点击,仅使用官方按钮,不再快进/重载视频。
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @match https://music.youtube.com/*
// @exclude https://studio.youtube.com/*
// @grant none
// @license MIT
// @noframes
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
/*********************
* Adjustable Parameters (Modify as needed)
* 可调参数(按需修改)
*********************/
const DEBUG = false; // Debug switch: true to output logs / 调试开关:true 输出调试日志
// ✅ 1. 不再隐藏广告:这里改为空数组,就不会再通过 CSS 隐藏任何广告元素
const CSS_HIDE_SELECTORS = []; // No ad hiding / 不隐藏广告
// ✅ 2. 不再移除广告 DOM
const REMOVE_PAIRS = []; // No DOM removal / 不移除广告 DOM
const CHECK_DEBOUNCE_MS = 150; // Debounce delay for triggering checks / 触发检测的去抖延时
const INTERVAL_CHECK_MS = 2000; // Fallback timer: Ad detection / 兜底定时:广告检测
const INTERVAL_CLEAN_MS = 4000; // Fallback timer: DOM cleanup / 兜底定时:清理广告 DOM(现在数组为空,基本无事可做)
const RELOAD_BASE_COOLDOWN_MS = 2000; // 保留但不再使用重载逻辑
const RELOAD_MAX_BACKOFF_MS = 30000; // 同上
const SEEK_EPSILON = 0.25; // 保留(已不再用来跳广告)
// 连环多广告检测参数(保留,用于连续广告时多次检查按钮)
const CHAIN_SKIP_MAX = 4;
const CHAIN_SKIP_DELAY_MS = 800;
/*********************
* Internal State
* 内部状态
*********************/
const state = {
skipping: false, // Re-entry lock / 防重入锁
lastReloadAt: 0, // 已不再使用重载,但先保留结构
reloadAttempts: 0,
lastReloadSignature: '',
scheduled: false, // Debounce schedule flag / 去抖调度标记
// ✅ 新增:记录“广告静音”前的视频状态
adMuted: false,
prevMuted: null,
prevVolume: null,
};
/*********************
* Helper Methods
* 便捷方法
*********************/
const log = (...args) => { if (DEBUG) console.log('[ASYA]', ...args); };
const now = () => Date.now();
const timeStr = () => new Date().toTimeString().split(' ', 1)[0];
const isMobile = location.hostname === 'm.youtube.com';
const isMusic = location.hostname === 'music.youtube.com';
const isShorts = () => location.pathname.startsWith('/shorts/');
function addCss() {
const sel = CSS_HIDE_SELECTORS.join(',');
if (!sel) return;
const style = document.createElement('style');
style.textContent = `${sel}{display:none !important;}`;
document.head ? document.head.appendChild(style) : document.documentElement.appendChild(style);
}
function removeAdElements() {
if (isShorts()) return;
for (const [outerSel, innerSel] of REMOVE_PAIRS) {
const outer = document.querySelector(outerSel);
if (!outer) continue;
const inner = outer.querySelector(innerSel);
if (!inner) continue;
outer.remove();
log('Removed ad block / 移除广告块:', outerSel, 'contains / 包含', innerSel);
}
}
// 查询跳过按钮(兼容不同形态)
function querySkipButton() {
const byClass = document.querySelector(
'.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-ad-skip-button-container button'
);
if (byClass) return byClass;
const btn = [...document.querySelectorAll('button')].find(b => {
const t = (b.getAttribute('aria-label') || b.textContent || '').trim();
return /skip ad|skip ads|跳过广告/i.test(t);
});
return btn || null;
}
// 广告上下文探测
function detectAdContext() {
const adShowing = !!document.querySelector('.ad-showing');
const pieCountdown = !!document.querySelector('.ytp-ad-timed-pie-countdown-container');
const survey = !!document.querySelector('.ytp-ad-survey-questions');
const skipBtn = querySkipButton();
const adLikely = adShowing || pieCountdown || survey || !!skipBtn;
return { adShowing, pieCountdown, survey, skipBtn, adLikely };
}
// 获取播放器引用
function getPlayers() {
const moviePlayerEl = document.querySelector('#movie_player') || null;
let playerEl = null;
let player = null;
if (isMobile || isMusic) {
playerEl = moviePlayerEl;
player = moviePlayerEl;
} else {
const ytd = document.querySelector('#ytd-player');
playerEl = ytd || moviePlayerEl || null;
if (ytd && typeof ytd.getPlayer === 'function') {
try { player = ytd.getPlayer(); } catch (_) {}
}
if (!player && moviePlayerEl) player = moviePlayerEl;
}
return { moviePlayerEl, playerEl, player };
}
//(原重载相关函数保留但不再调用)
function safeLoadByVars(players, videoId, start) {
const list = [players.player, players.playerEl, players.moviePlayerEl].filter(Boolean);
for (const p of list) {
if (typeof p.loadVideoWithPlayerVars === 'function') {
p.loadVideoWithPlayerVars({ videoId, start });
return true;
}
if (typeof p.loadVideoByPlayerVars === 'function') {
p.loadVideoByPlayerVars({ videoId, start });
return true;
}
}
if (players.player && typeof players.player.seekTo === 'function') {
players.player.seekTo(start, true);
return true;
}
return false;
}
function restoreSubtitlesIfNeeded(moviePlayerEl, wantOn) {
if (!moviePlayerEl) return;
if (typeof moviePlayerEl.isSubtitlesOn !== 'function' ||
typeof moviePlayerEl.toggleSubtitlesOn !== 'function') return;
const start = now();
const timer = setInterval(() => {
if (now() - start > 5000) { clearInterval(timer); return; }
try {
const cur = !!moviePlayerEl.isSubtitlesOn();
if (wantOn && !cur) {
moviePlayerEl.toggleSubtitlesOn();
clearInterval(timer);
} else if (!wantOn && cur) {
moviePlayerEl.toggleSubtitlesOn();
clearInterval(timer);
} else {
clearInterval(timer);
}
} catch (_) {}
}, 250);
}
/*********************
* ✅ 新增:只负责“广告时静音 / 非广告时还原”
*********************/
function ensureAdMute(ctx) {
const video = document.querySelector('video.html5-main-video');
if (!video) return;
if (ctx.adLikely) {
// 进入广告:记录当前静音与音量,并静音
if (!state.adMuted) {
state.adMuted = true;
state.prevMuted = video.muted;
state.prevVolume = video.volume;
if (DEBUG) log('Ad detected, muting video / 检测到广告,开始静音');
}
video.muted = true;
try { video.volume = 0; } catch (_) {}
} else {
// 离开广告:还原之前的声音状态
if (state.adMuted) {
if (DEBUG) log('Ad ended, restore volume / 广告结束,还原音量');
if (state.prevMuted !== null) {
video.muted = state.prevMuted;
}
if (typeof state.prevVolume === 'number') {
try { video.volume = state.prevVolume; } catch (_) {}
}
state.adMuted = false;
state.prevMuted = null;
state.prevVolume = null;
}
}
}
/*********************
* ✅ 改造后的“软跳过”:只点 YouTube 自带的“跳过广告”按钮
* 不再做静音+跳尾;不再 seekTo;不再重载
*********************/
function trySoftSkip(players, ctx) {
if (ctx.skipBtn) {
try {
ctx.skipBtn.click();
log('Clicked Skip Button / 点击跳过按钮');
return true;
} catch (_) {
// ignore
}
}
return false;
}
// 重载兜底逻辑保留但不再调用(你如果以后想恢复“暴力穿广告”,可以再接回去)
function tryHeavyReload(players) {
if (!players.player ||
typeof players.player.getVideoData !== 'function' ||
typeof players.player.getCurrentTime !== 'function') {
return false;
}
const data = players.player.getVideoData();
const vid = data && data.video_id;
const start = Math.floor(players.player.getCurrentTime());
if (!vid || !Number.isFinite(start)) return false;
const signature = `${vid}:${Math.floor(start / 5)}`;
const nowTs = now();
if (signature === state.lastReloadSignature) {
const backoff = Math.min(RELOAD_MAX_BACKOFF_MS, RELOAD_BASE_COOLDOWN_MS * Math.pow(2, state.reloadAttempts));
if (nowTs - state.lastReloadAt < backoff) {
log('Reload Cooldown, skipping: / 重载冷却中,跳过本次:', backoff - (nowTs - state.lastReloadAt), 'ms');
return false;
}
} else {
state.reloadAttempts = 0;
}
let wantSubsOn = false;
if (players.moviePlayerEl &&
typeof players.moviePlayerEl.isSubtitlesOn === 'function') {
try { wantSubsOn = !!players.moviePlayerEl.isSubtitlesOn(); } catch (_) {}
}
const ok = safeLoadByVars(players, vid, start);
if (ok) {
state.lastReloadSignature = signature;
state.lastReloadAt = nowTs;
state.reloadAttempts += 1;
log('Executed Heavy Reload thru Ad / 执行重载穿过广告:', { vid, start, attempts: state.reloadAttempts, t: timeStr() });
restoreSubtitlesIfNeeded(players.moviePlayerEl, wantSubsOn);
return true;
}
return false;
}
// 连环检测:多条广告时多试几次
function chainSkipIfNeeded() {
let count = 0;
const loop = () => {
if (count >= CHAIN_SKIP_MAX) return;
count++;
const ctx = detectAdContext();
if (!ctx.adLikely) return;
if (!state.skipping) {
skipAd(true);
}
if (count < CHAIN_SKIP_MAX) {
setTimeout(loop, CHAIN_SKIP_DELAY_MS);
}
};
setTimeout(loop, CHAIN_SKIP_DELAY_MS);
}
// 主流程
function skipAd(fromChain) {
if (isShorts()) return; // Shorts 不动
if (state.skipping) return;
state.skipping = true;
let acted = false;
try {
const ctx = detectAdContext();
// ✅ 无论是否有广告,每次检查都先处理“静音/还原”
ensureAdMute(ctx);
// 没广告了就只需要上面那行的“还原音量”,直接结束
if (!ctx.adLikely) return;
const players = getPlayers();
if (!players.player && !players.playerEl && !players.moviePlayerEl) return;
// ✅ 只做“官方按钮跳过”
const softOK = trySoftSkip(players, ctx);
if (softOK) {
acted = true;
}
// ❌ 不再调用重载兜底:保留广告,只是静音 + 能跳的时候自动跳
// const heavyOK = tryHeavyReload(players);
// if (heavyOK) {
// acted = true;
// }
} finally {
state.skipping = false;
// 如果刚刚真的点过“跳过广告”,为了连续广告的情况,跑一轮连环检测
if (acted && !fromChain) {
chainSkipIfNeeded();
}
}
}
// 去抖调度
function scheduleCheck(delay = CHECK_DEBOUNCE_MS) {
if (state.scheduled) return;
state.scheduled = true;
setTimeout(() => {
state.scheduled = false;
skipAd();
}, delay);
}
// 观察器:DOM 有变化时触发检查(广告出现/结束都会动 DOM)
function setupObserver() {
const target = document.body || document.documentElement;
if (!target) return;
const mo = new MutationObserver(() => {
scheduleCheck(50);
});
mo.observe(target, {
attributes: true,
childList: true,
subtree: true
});
}
/*********************
* Start / 启动
*********************/
addCss(); // 现在 CSS 选择器是空的,不会隐藏内容
removeAdElements(); // 现在数组为空,不会真的移除东西
setupObserver();
scheduleCheck(0); // 刚加载时先检查一次(如果一进来就有广告)
// 兜底定时器:即使 MutationObserver 漏掉了,也会定期检查
setInterval(() => scheduleCheck(), INTERVAL_CHECK_MS);
setInterval(() => removeAdElements(), INTERVAL_CLEAN_MS);
})();