// ==UserScript==
// @name Smart Auto Skip YouTube Ads (Enhanced+ 8.5.0 with Volume Control)
// @namespace https://github.com/tientq64/userscripts
// @version 8.7.0
// @description 智能跳过/弱化 YouTube 广告:强化新倒计时检测、按钮重试、速率钉住、失败兜底时间恢复刷新,并添加音量恢复功能
// @author tientq64 + enhanced by yy
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @match https://music.youtube.com/*
// @exclude https://studio.youtube.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict'
const isMusic = location.hostname === 'music.youtube.com'
const isShorts = location.pathname.startsWith('/shorts/')
const isVideoPage = !isMusic
// 音量状态保存变量
let originalVolume = 1
let originalMuted = false
let hasSavedVolumeState = false
// 软跳转节流键与状态
const RELOAD_FLAG_KEY = '__yt_ad_reload_flag__'
const RELOAD_TS_KEY = '__yt_ad_reload_ts__'
const RELOAD_URL_KEY = '__yt_ad_reload_url__'
const RELOAD_VOL_KEY = '__yt_ad_reload_volume__' // 新增:存储音量状态
const RELOAD_MUTE_KEY = '__yt_ad_reload_muted__' // 新增:存储静音状态
const RELOAD_COOLDOWN_MS = 60_000 // 1 分钟内不重复触发软跳转
// 进入页面若存在软跳转状态,恢复时间点和音量
;(function restoreAfterBypass() {
try {
const flag = sessionStorage.getItem(RELOAD_FLAG_KEY)
const ts = Number(sessionStorage.getItem(RELOAD_TS_KEY) || 0)
const url = sessionStorage.getItem(RELOAD_URL_KEY)
if (flag === '1' && url && Math.abs(Date.now() - ts) < 20_000) {
// 恢复后立刻清理,防止回环
sessionStorage.removeItem(RELOAD_FLAG_KEY)
sessionStorage.removeItem(RELOAD_TS_KEY)
sessionStorage.removeItem(RELOAD_URL_KEY)
// 获取保存的音量状态
const savedVolume = sessionStorage.getItem(RELOAD_VOL_KEY)
const savedMuted = sessionStorage.getItem(RELOAD_MUTE_KEY)
if (savedVolume) originalVolume = parseFloat(savedVolume)
if (savedMuted) originalMuted = savedMuted === 'true'
sessionStorage.removeItem(RELOAD_VOL_KEY)
sessionStorage.removeItem(RELOAD_MUTE_KEY)
// 等播放就绪后 seek
const trySeek = () => {
const v = document.querySelector('video.html5-main-video')
if (v && !isNaN(v.duration) && v.duration > 1) {
v.currentTime = Math.max(0, (new URL(url)).searchParams.get('t') ? v.currentTime : v.currentTime)
// 用存储进度恢复
const target = Number((window.__yt_ad_restore_time__ || 0))
if (target > 0) v.currentTime = target + 1
delete window.__yt_ad_restore_time__
// 恢复原始音量状态
v.volume = originalVolume
v.muted = originalMuted
} else {
requestAnimationFrame(trySeek)
}
}
requestAnimationFrame(trySeek)
} else {
// 清理过期
sessionStorage.removeItem(RELOAD_FLAG_KEY)
sessionStorage.removeItem(RELOAD_TS_KEY)
sessionStorage.removeItem(RELOAD_URL_KEY)
sessionStorage.removeItem(RELOAD_VOL_KEY)
sessionStorage.removeItem(RELOAD_MUTE_KEY)
}
} catch {}
})()
function simulateClick(el) {
if (!el) return
['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }))
})
}
function findVisibleAdButtons() {
const selectors = [
'.ytp-ad-skip-button',
'.ytp-skip-ad-button',
'[id^="skip-button"]',
'.ytp-ad-player-overlay-layout__skip-or-preview-container button',
'.ytp-ad-player-overlay-layout__skip-or-preview-container .ytp-skip-ad-button',
'.ytp-ad-skip-button-modern', // 新样式
'button[aria-label*="Skip"], button[aria-label*="跳过"]',
]
const buttons = []
selectors.forEach(sel => {
document.querySelectorAll(sel).forEach(btn => {
const rect = btn.getBoundingClientRect()
const visible = btn.offsetParent !== null && rect.width > 0 && rect.height > 0 && getComputedStyle(btn).visibility !== 'hidden'
if (visible) buttons.push(btn)
})
})
return buttons
}
function tryClickAdButton(retries = 30, delay = 400) {
const buttons = findVisibleAdButtons()
if (buttons.length > 0) {
buttons.forEach(btn => simulateClick(btn))
console.log(`[Ad Skipped] ${buttons.length} button(s) clicked`)
// 广告跳过,恢复音量状态
restoreVolumeState()
} else if (retries > 0) {
setTimeout(() => tryClickAdButton(retries - 1, delay), delay)
}
}
function removeResidualAdUI() {
const selectors = [
'.ytp-ad-skip-button',
'.ytp-skip-ad-button',
'.ytp-ad-player-overlay-layout__skip-or-preview-container',
'.ytp-ad-player-overlay',
'.ytp-ad-image-overlay',
'.ytp-ad-overlay-container',
'.ytp-ad-end-screen',
'.ytp-ad-text-overlay',
'.ytp-ad-preview-container',
'.ytp-ad-spinner',
'.ytp-ad-duration-remaining',
'.ytp-ad-timed-pie-countdown-container',
'[class*="ad-countdown"]',
'.ytp-paid-content-overlay'
]
selectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => el.remove())
})
}
function isAdPlaying() {
// 覆盖新的计时器/沙漏/广告态
return document.querySelector(
'.ad-showing, .ytp-ad-player-overlay, .ytp-ad-spinner, .ytp-ad-duration-remaining, .ytp-ad-timed-pie-countdown-container, [class*="ad-countdown"]'
)
}
// 保存当前音量状态
function saveVolumeState() {
if (hasSavedVolumeState) return
const v = document.querySelector('video.html5-main-video')
if (v) {
originalVolume = v.volume
originalMuted = v.muted
hasSavedVolumeState = true
console.log('[Volume State] Saved:', { volume: originalVolume, muted: originalMuted })
}
}
// 恢复原始音量状态
function restoreVolumeState() {
const v = document.querySelector('video.html5-main-video')
if (v && hasSavedVolumeState) {
v.volume = originalVolume
v.muted = originalMuted
console.log('[Volume State] Restored:', { volume: originalVolume, muted: originalMuted })
hasSavedVolumeState = false
}
}
// 层1:速率钉住器(持续对抗回滚/钳制)
let ratePinTimer = null
function pinAdPlaybackRate(enable) {
if (!enable) {
if (ratePinTimer) cancelAnimationFrame(ratePinTimer), ratePinTimer = null
// 停止加速时恢复音量
restoreVolumeState()
return
}
// 开始加速前保存音量状态
saveVolumeState()
const loop = () => {
const v = document.querySelector('video.html5-main-video')
if (v) {
// 多属性联动,尽量绕过限制
try {
v.muted = true // 广告期间保持静音
v.defaultPlaybackRate = 16
if (v.playbackRate !== 16) v.playbackRate = 16
// 关闭音高保持,规避卡速
if ('preservesPitch' in v) v.preservesPitch = false
if ('mozPreservesPitch' in v) v.mozPreservesPitch = false
if ('webkitPreservesPitch' in v) v.webkitPreservesPitch = false
// 避免暂停状态
if (v.paused) v.play().catch(()=>{})
} catch {}
}
ratePinTimer = requestAnimationFrame(loop)
}
if (!ratePinTimer) ratePinTimer = requestAnimationFrame(loop)
}
// 层2:常规跳过流程
function attemptSkipPath() {
if (isShorts) return
if (!isAdPlaying()) {
// 如果没有广告,确保音量状态已恢复
restoreVolumeState()
return
}
const moviePlayerEl = document.querySelector('#movie_player')
// 优先调用原生 skip
if (moviePlayerEl?.skipAd) {
try {
moviePlayerEl.skipAd()
console.log('[Ad Skipped] via moviePlayer.skipAd()')
// 广告跳过,恢复音量状态
restoreVolumeState()
return
} catch {}
}
// 点击按钮 + 速率钉住
tryClickAdButton()
pinAdPlaybackRate(true)
// 有些场景 seekTo 在广告期被拦截,但我们保持速率钉住直到广告态消失
removeResidualAdUI()
}
// 层3:兜底软跳转(进度恢复刷新)
// 触发条件:广告状态持续一段时间仍未解除,且不在冷却期
let adStartAt = 0
function considerSoftBypass() {
const adNow = !!isAdPlaying()
const v = document.querySelector('video.html5-main-video')
if (adNow && adStartAt === 0) adStartAt = Date.now()
if (!adNow) {
// 广告结束,恢复音量状态
restoreVolumeState()
pinAdPlaybackRate(false)
adStartAt = 0
return
}
// 广告持续超过阈值,尝试软跳转
const AD_STUCK_MS = 5000 // 5s 认为被"困住"
if (adNow && adStartAt > 0 && Date.now() - adStartAt > AD_STUCK_MS) {
const lastReload = Number(sessionStorage.getItem(RELOAD_TS_KEY) || 0)
if (Date.now() - lastReload < RELOAD_COOLDOWN_MS) return
// 保存当前音量状态
saveVolumeState()
// 记录进度与 URL,刷新后恢复
try {
let currentTime = 0
if (v && !isNaN(v.currentTime)) currentTime = Math.floor(v.currentTime || 0)
window.__yt_ad_restore_time__ = currentTime
sessionStorage.setItem(RELOAD_FLAG_KEY, '1')
sessionStorage.setItem(RELOAD_TS_KEY, String(Date.now()))
sessionStorage.setItem(RELOAD_URL_KEY, location.href)
sessionStorage.setItem(RELOAD_VOL_KEY, String(originalVolume))
sessionStorage.setItem(RELOAD_MUTE_KEY, String(originalMuted))
console.log('[Ad Bypass] soft-reload with time restore', currentTime)
// 使用 history.replaceState + location.reload,避免新开 tab
history.replaceState(null, '', location.href)
location.reload()
} catch (e) {
console.warn('Soft bypass failed', e)
} finally {
adStartAt = 0
pinAdPlaybackRate(false)
}
}
}
function hideAdElements() {
const selectors = [
'#player-ads',
'#masthead-ad',
'.ytp-featured-product',
'.yt-mealbar-promo-renderer',
'ytd-merch-shelf-renderer',
'ytmusic-mealbar-promo-renderer',
'ytmusic-statement-banner-renderer',
'#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'
]
const style = document.createElement('style')
style.textContent = selectors.join(',') + '{ display: none !important; }'
document.head.appendChild(style)
}
function removeInlineAds() {
const adSelectors = [
['ytd-reel-video-renderer', '.ytd-ad-slot-renderer']
]
for (const [container, child] of adSelectors) {
const el = document.querySelector(container)
if (el?.querySelector(child)) el.remove()
}
}
hideAdElements()
if (isVideoPage) {
setInterval(removeInlineAds, 1000)
setInterval(removeResidualAdUI, 1000)
removeInlineAds()
removeResidualAdUI()
}
// 监听 DOM 变化:尝试跳过 + 兜底判定
const observer = new MutationObserver(() => {
setTimeout(() => {
attemptSkipPath()
considerSoftBypass()
removeResidualAdUI()
}, 150)
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style', 'hidden', 'aria-hidden']
})
// 周期性心跳,防止遗漏
setInterval(() => {
attemptSkipPath()
considerSoftBypass()
}, 400)
})()