// ==UserScript==
// @name YouTube Mute and Skip Ads
// @namespace https://github.com/ion1/userscripts
// @version 0.0.30
// @author ion
// @description Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @homepage https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
// @homepageURL https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
// @match *://www.youtube.com/*
// @match *://music.youtube.com/*
// @grant GM_addStyle
// @run-at document-body
// ==/UserScript==
(e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const n=document.createElement("style");n.textContent=e,document.head.append(n)})(` /* Keep these in sync with the watchers. */
#movie_player
:is(.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button) {
anchor-name: --youtube-mute-skip-ads-unclickable-button;
}
body:has(
#movie_player
:is(
.ytp-ad-skip-button,
.ytp-ad-skip-button-modern,
.ytp-skip-ad-button
):not([style*="display: none"], [aria-hidden="true"])
)::after {
content: "\u{1D606}\u{1D5FC}\u{1D602}\u{1D601}\u{1D602}\u{1D5EF}\u{1D5F2}-\u{1D5FA}\u{1D602}\u{1D601}\u{1D5F2}-\u{1D600}\u{1D5F8}\u{1D5F6}\u{1D5FD}-\u{1D5EE}\u{1D5F1}\u{1D600}\\A\\A"
"Unfortunately, YouTube has started to block automated clicks based on isTrusted being false.\\A\\A"
"Please click on the skip button manually.";
white-space: pre-line;
pointer-events: none;
z-index: 9999;
position: fixed;
position-anchor: --youtube-mute-skip-ads-unclickable-button;
padding: 1.5em;
border-radius: 1.5em;
margin-bottom: 1em;
bottom: anchor(--youtube-mute-skip-ads-unclickable-button top);
right: anchor(--youtube-mute-skip-ads-unclickable-button right);
max-width: 25em;
font-size: 1.4rem;
line-height: 2rem;
font-weight: 400;
color: rgb(240 240 240);
background-color: rgb(0 0 0 / 0.7);
backdrop-filter: blur(10px);
animation: fade-in 3s linear;
}
@keyframes fade-in {
0% {
opacity: 0;
}
67% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#movie_player.ad-showing video,
#shorts-player.ad-created video {
filter: blur(100px) opacity(0.25) grayscale(0.5);
}
#movie_player.ad-showing .ytp-title,
#movie_player.ad-showing .ytp-title-channel,
.ytp-visit-advertiser-link,
.ytp-ad-visit-advertiser-button,
ytmusic-app:has(#movie_player.ad-showing)
ytmusic-player-bar
:is(.title, .subtitle) {
filter: blur(4px) opacity(0.5) grayscale(0.5);
transition: 0.05s filter linear;
}
:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-visit-advertiser-link,.ytp-ad-visit-advertiser-button,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
filter: none;
}
/* These popups are showing up on top of the video with a hidden dismiss button
* since 2024-09-25.
*/
.ytp-suggested-action-badge {
visibility: hidden !important;
}
#movie_player.ad-showing .caption-window,
.ytp-ad-player-overlay-flyout-cta,
.ytp-ad-player-overlay-layout__player-card-container, /* Seen since 2024-04-06. */
.ytp-ad-action-interstitial-slot, /* Added on 2024-08-25. */
ytd-action-companion-ad-renderer,
ytd-display-ad-renderer,
ytd-ad-slot-renderer,
ytd-promoted-sparkles-web-renderer,
ytd-player-legacy-desktop-watch-ads-renderer,
ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],
ytd-merch-shelf-renderer {
filter: blur(10px) opacity(0.25) grayscale(0.5);
transition: 0.05s filter linear;
}
:is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,.ytp-ad-player-overlay-layout__player-card-container,.ytp-ad-action-interstitial-slot,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-player-legacy-desktop-watch-ads-renderer,ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],ytd-merch-shelf-renderer):is(:hover,:focus-within) {
filter: none;
}
.ytp-ad-action-interstitial-background-container /* Added on 2024-08-25. */ {
/* An image ad in place of the video. */
filter: blur(10px) opacity(0.25) grayscale(0.5);
} `);
(function () {
'use strict';
const debugging = false;
const logPrefix = "[youtube-mute-skip-ads]";
function error(...args) {
console.error(logPrefix, ...args);
}
function warn(...args) {
console.warn(logPrefix, ...args);
}
function info(...args) {
console.info(logPrefix, ...args);
}
function abortableSetTimeout(signal, callback, timeout) {
if (signal?.aborted) return;
const timeoutId = setTimeout(() => {
signal?.removeEventListener("abort", clearThisTimeout);
if (signal?.aborted) return;
callback();
}, timeout);
function clearThisTimeout() {
clearTimeout(timeoutId);
}
signal?.addEventListener("abort", clearThisTimeout, { once: true });
}
function abortableSetInterval(signal, callback, interval) {
if (signal?.aborted) return;
const intervalId = setInterval(() => {
if (signal?.aborted) return;
callback();
}, interval);
function clearThisInterval() {
clearInterval(intervalId);
}
signal?.addEventListener("abort", clearThisInterval, { once: true });
}
function observeSelector({
selector,
matcher,
root,
name,
signal,
onAdded
}) {
root ??= document;
matcher ??= selector;
if (signal?.aborted) return;
const abortControllerMap = /* @__PURE__ */ new Map();
function added(elem) {
const abortController = new AbortController();
abortControllerMap.set(elem, abortController);
try {
onAdded({ elem, signal: abortController.signal });
} catch (err) {
reportError(err);
}
}
function removed(elem) {
const abortController = abortControllerMap.get(elem);
if (abortController) {
abortControllerMap.delete(elem);
abortController.abort();
}
}
for (const elem of root.querySelectorAll(selector)) {
if (elem.matches(matcher)) {
added(elem);
}
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
for (const elem of matchingElementsInTree({
root: addedNode,
selector,
matcher
})) {
added(elem);
}
}
for (const removedNode of mutation.removedNodes) {
for (const elem of matchingElementsInTree({
root: removedNode,
selector,
matcher
})) {
removed(elem);
}
}
}
});
observer.observe(root, { childList: true, subtree: true });
signal?.addEventListener(
"abort",
() => {
observer.disconnect();
while (abortControllerMap.size > 0) {
const elem = abortControllerMap.keys().next().value;
removed(elem);
}
},
{ once: true }
);
}
function* matchingElementsInTree({
root,
selector,
matcher
}) {
if (!(root instanceof Element)) return;
if (root.matches(matcher)) {
yield root;
return;
}
for (const descendant of root.querySelectorAll(selector)) {
if (descendant.matches(matcher)) {
yield descendant;
}
}
}
function observeVisible({
elem,
name,
periodicCheckInterval,
signal,
onVisible
}) {
if (signal?.aborted) return;
periodicCheckInterval ??= 1e4;
let abortController = null;
let checkVisiblePending = false;
function checkVisible() {
if (checkVisiblePending) return;
checkVisiblePending = true;
abortableSetTimeout(signal, () => {
checkVisiblePending = false;
checkVisibleImmediately();
});
}
function checkVisibleImmediately() {
const visible = isElementVisible(elem);
if (visible && abortController === null) {
abortController = new AbortController();
try {
onVisible({ elem, signal: abortController.signal });
} catch (err) {
reportError(err);
}
} else if (!visible && abortController !== null) {
abortController.abort();
abortController = null;
}
}
checkVisibleImmediately();
const observer = new MutationObserver(checkVisible);
for (let ancestor = elem; ancestor !== null; ancestor = ancestor.parentElement) {
observer.observe(ancestor, {
attributes: true,
attributeFilter: ["style", "class"],
subtree: false
});
}
abortableSetInterval(signal, checkVisible, periodicCheckInterval);
signal?.addEventListener(
"abort",
() => {
observer.disconnect();
abortController?.abort();
abortController = null;
},
{ once: true }
);
}
function isElementVisible(elem) {
for (let ancestor = elem; ancestor !== null; ancestor = ancestor.parentElement) {
const style = getComputedStyle(ancestor);
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.opacity === "0")
return false;
}
return true;
}
function observeAttr({
elem,
name,
attr,
shouldGetAttr,
signal,
onChanged
}) {
shouldGetAttr ??= true;
if (signal?.aborted) return;
const observer = new MutationObserver((_mutations) => {
onChanged({
elem,
attr,
value: shouldGetAttr ? elem.getAttribute(attr) : null
});
});
observer.observe(elem, {
attributeFilter: [attr],
attributes: true
});
signal?.addEventListener(
"abort",
() => {
observer.disconnect();
},
{ once: true }
);
}
function observeHasClass({
elem,
name,
className,
signal,
onAdded
}) {
if (signal?.aborted) return;
let abortController = null;
function change() {
const elemHasClass = elem.classList.contains(className);
if (abortController === null && elemHasClass) {
abortController = new AbortController();
try {
onAdded({ elem, className, signal: abortController.signal });
} catch (err) {
reportError(err);
}
} else if (abortController !== null && !elemHasClass) {
abortController.abort();
abortController = null;
}
}
observeAttr({
elem,
name,
attr: "class",
shouldGetAttr: false,
signal,
onChanged: change
});
signal?.addEventListener(
"abort",
() => {
abortController?.abort();
abortController = null;
},
{ once: true }
);
}
const videoSelector = "#movie_player video";
const muteButtonSelector = ":is(.ytp-mute-button, ytdDesktopShortsVolumeControlsMuteIconButton, ytmusic-player-bar tp-yt-paper-icon-button.volume)";
const shortsRendererSelector = "ytd-reel-video-renderer";
const shortsVideoSelector = "#shorts-player video";
const shortsUpButtonSelector = "#navigation-button-up button";
const shortsDownButtonSelector = "#navigation-button-down button";
function getVideoElement() {
return getVideoElementBySelector(videoSelector);
}
function getShortsVideoElement() {
return getVideoElementBySelector(shortsVideoSelector);
}
function getVideoElementBySelector(selector) {
const videoElem = getHTMLElementBySelector(selector);
if (!(videoElem instanceof HTMLVideoElement)) {
error(
"Expected",
JSON.stringify(videoSelector),
"to be a video element, got:",
videoElem?.cloneNode(true)
);
return null;
}
return videoElem;
}
function getShortsUpButton() {
return getHTMLElementBySelector(shortsUpButtonSelector);
}
function getShortsDownButton() {
return getHTMLElementBySelector(shortsDownButtonSelector);
}
function getHTMLElementBySelector(selector) {
for (const elem of document.querySelectorAll(selector)) {
if (!(elem instanceof HTMLElement)) {
error(
"Expected",
JSON.stringify(muteButtonSelector),
"to be an HTML element, got:",
elem.cloneNode(true)
);
continue;
}
return elem;
}
error("Failed to find", JSON.stringify(selector));
return null;
}
function getShortsParentElement(elemWithinShort) {
const shortsRenderer = elemWithinShort.closest(shortsRendererSelector);
if (shortsRenderer == null) {
return null;
}
return shortsRenderer.parentElement;
}
function callMoviePlayerMethod(name, onSuccess, args) {
try {
const movieElem = document.getElementById("movie_player");
if (movieElem == null) {
warn("movie_player element not found");
return;
}
const method = Object.getOwnPropertyDescriptor(
movieElem,
name
)?.value;
if (method == null) {
warn(`movie_player element has no ${JSON.stringify(name)} property`);
return;
}
if (!(typeof method === "function")) {
warn(
`movie_player element property ${JSON.stringify(name)} is not a function`
);
return;
}
const result = method.apply(movieElem, args);
if (onSuccess != null) {
onSuccess(result);
}
return result;
} catch (e) {
warn(`movie_player method ${JSON.stringify(name)} failed:`, e);
return;
}
}
function disableVisibilityChecks() {
for (const eventName of ["visibilitychange", "blur", "focus"]) {
document.addEventListener(
eventName,
(ev) => {
ev.stopImmediatePropagation();
},
{ capture: true }
);
}
document.hasFocus = () => true;
Object.defineProperties(document, {
visibilityState: { value: "visible" },
hidden: { value: false }
});
}
function main() {
disableVisibilityChecks();
const adPlayerOverlaySelectors = [
".ytp-ad-player-overlay",
".ytp-ad-player-overlay-layout"
// Seen since 2024-04-06.
];
for (const adPlayerOverlaySelector of adPlayerOverlaySelectors) {
observeSelector({
selector: adPlayerOverlaySelector,
name: adPlayerOverlaySelector,
onAdded: adIsPlaying
});
}
let previousShortsParent = null;
let currentShortsParent = null;
observeSelector({
selector: "#shorts-player",
name: "#shorts-player",
onAdded({ elem: shortsRenderer, signal }) {
const shortsParent = getShortsParentElement(shortsRenderer);
if (shortsParent && shortsParent.isConnected) {
if (shortsParent !== currentShortsParent?.deref()) {
if (debugging) ;
[previousShortsParent, currentShortsParent] = [
currentShortsParent,
new WeakRef(shortsParent)
];
}
} else {
previousShortsParent = null;
currentShortsParent = null;
}
let wentBackwards = false;
if (previousShortsParent != null && currentShortsParent != null) {
const previousShortsParentDeref = previousShortsParent.deref();
const currentShortsParentDeref = currentShortsParent.deref();
if (previousShortsParentDeref != null && currentShortsParentDeref != null) {
const pos = previousShortsParentDeref.compareDocumentPosition(
currentShortsParentDeref
);
if ((pos & Node.DOCUMENT_POSITION_DISCONNECTED) === 0 && (pos & Node.DOCUMENT_POSITION_PRECEDING) !== 0) {
wentBackwards = true;
}
if (debugging) ;
}
}
observeHasClass({
elem: shortsRenderer,
name: "#shorts-player",
className: "ad-created",
signal,
onAdded() {
shortsMuteAd();
}
});
if (shortsRenderer.classList.contains("ad-created")) {
shortsSkipAd({ signal, wentBackwards });
}
}
});
observeSelector({
selector: "#movie_player",
name: "#movie_player",
onAdded({ elem: moviePlayer, signal }) {
const adSkipButtonSelectors = [
".ytp-ad-skip-button",
".ytp-ad-skip-button-modern",
// Seen since 2023-11-10.
".ytp-skip-ad-button"
// Seen since 2024-04-06.
];
for (const adSkipButtonSelector of adSkipButtonSelectors) {
const name = `#movie_player ${adSkipButtonSelector}`;
observeSelector({
root: moviePlayer,
selector: adSkipButtonSelector,
name,
signal,
onAdded({ elem: button, signal: signal2 }) {
observeVisible({
elem: button,
name,
signal: signal2,
onVisible({ signal: signal3 }) {
observeAttr({
elem: button,
name,
attr: "aria-hidden",
signal: signal3,
onChanged(ariaHidden) {
if (ariaHidden === null) {
click(button, `skip (${adSkipButtonSelector})`);
}
}
});
}
});
}
});
}
}
});
observeSelector({
selector: ".ytp-ad-overlay-close-button",
name: ".ytp-ad-overlay-close-button",
onAdded({ elem: button, signal }) {
observeVisible({
elem: button,
name: ".ytp-ad-overlay-close-button",
signal,
onVisible() {
click(button, ".ytp-ad-overlay-close-button");
}
});
}
});
observeSelector({
selector: "ytmusic-you-there-renderer button",
name: "are-you-there",
onAdded({ elem: button, signal }) {
observeVisible({
elem: button,
name: "are-you-there",
signal,
onVisible() {
click(button, "are-you-there");
}
});
}
});
}
function adIsPlaying({ signal }) {
info("An ad is playing, muting and speeding up");
const video = getVideoElement();
if (video == null) {
return;
}
mute(video);
speedup(video, signal);
cancelPlayback(video, signal);
}
function shortsMuteAd() {
info("A shorts ad is playing, muting");
const video = getShortsVideoElement();
if (video != null) {
mute(video);
}
}
function shortsSkipAd({
signal,
wentBackwards
}) {
info("A shorts ad is playing, skipping");
const [direction, button] = wentBackwards ? ["up", getShortsUpButton()] : ["down", getShortsDownButton()];
const video = getShortsVideoElement();
if (video == null) return;
if (button == null) {
return;
}
oncePlaying({
elem: video,
signal,
onWaiting() {
},
onPlaying() {
click(button, `${direction} button`);
}
});
}
function mute(video) {
video.muted = true;
}
function speedup(video, signal) {
for (let rate = 16; rate >= 2; rate /= 2) {
try {
video.playbackRate = rate;
break;
} catch (e) {
}
}
function onRemoved() {
const originalRate = callMoviePlayerMethod("getPlaybackRate");
if (originalRate == null || typeof originalRate !== "number" || isNaN(originalRate)) {
warn(
`Restoring playback rate failed:`,
`unable to query the current playback rate, got: ${JSON.stringify(originalRate)}.`,
`Falling back to 1.`
);
restorePlaybackRate(video, 1);
return;
}
restorePlaybackRate(video, originalRate);
}
signal?.addEventListener("abort", onRemoved, { once: true });
}
function restorePlaybackRate(video, originalRate) {
try {
video.playbackRate = originalRate;
} catch (e) {
}
}
function cancelPlayback(video, signal) {
function doCancelPlayback() {
info("Attempting to cancel playback");
callMoviePlayerMethod("cancelPlayback", () => {
signal.addEventListener(
"abort",
() => {
resumePlaybackIfNotAtEnd();
},
{ once: true }
);
});
}
oncePlaying({
elem: video,
signal,
onWaiting() {
},
onPlaying: doCancelPlayback
});
}
function oncePlaying({
elem,
signal,
onWaiting,
onPlaying
}) {
if (elem.paused || elem.readyState < 3) {
onWaiting?.();
elem.addEventListener(
"playing",
() => {
onPlaying();
},
{ signal, once: true }
);
} else {
onPlaying();
}
}
function resumePlaybackIfNotAtEnd() {
const currentTime = callMoviePlayerMethod("getCurrentTime");
const duration = callMoviePlayerMethod("getDuration");
const isAtLiveHead = callMoviePlayerMethod("isAtLiveHead");
if (currentTime == null || duration == null || typeof currentTime !== "number" || typeof duration !== "number" || isNaN(currentTime) || isNaN(duration)) {
warn(
`movie_player methods getCurrentTime/getDuration failed, got time: ${JSON.stringify(currentTime)}, duration: ${JSON.stringify(duration)}`
);
return;
}
if (isAtLiveHead == null || typeof isAtLiveHead !== "boolean") {
warn(
`movie_player method isAtLiveHead failed, got: ${JSON.stringify(isAtLiveHead)}`
);
return;
}
const atEnd = duration - currentTime < 1;
if (atEnd && !isAtLiveHead) {
info(
`Video is at the end (${currentTime}/${duration}), not attempting to resume playback`
);
return;
}
info("Attempting to resume playback");
callMoviePlayerMethod("playVideo");
}
function click(elem, description) {
if (!(elem instanceof HTMLElement)) return;
if (elem.getAttribute("aria-hidden")) {
info("Not clicking (aria-hidden):", description);
} else {
info("Clicking:", description);
elem.click();
}
}
main();
})();