YouTube is a trillion-dollar company. Mute Tube is a few hundred lines of JavaScript. The score is roughly even.
This is Tom and Jerry. Has been for years. YouTube updates something — renames a class, adds a new ad format, tweaks their detection logic — and within days someone notices, patches the extension, and pushes it public. YouTube has more engineers. The community has more time and more motivation. Neither side ever fully wins.
The current version of Mute Tube is not the first version. It will not be the last. If you clone the repo and look at the selector array, you can read the history of the chase in the code itself.
const adSelectors = [
'.video-ads',
'.ytp-ad-player-overlay',
'.ytp-ad-overlay-container',
'.ytp-ad-skip-button',
'.ytp-ad-skip-button-modern',
'[class*="ad-showing"]'
];
Six selectors for the same thing. That list is a fossil record. .ytp-ad-skip-button worked. Then YouTube introduced .ytp-ad-skip-button-modern. Then they added .ytp-skip-ad-button. The wildcard [class*="ad-showing"] is the catch-all added when they started rotating names — anything with "ad-showing" in the class, whatever they decide to call it this week. Each entry in that array is a round YouTube thought they won and didn't.
Don't Block the Request. Let YouTube Think It Won.
The core approach is dumb in the best way. Do not block the ad request. Do not touch the network. Let YouTube think it won.
function detectAndMuteAds() {
const video = document.querySelector('video');
const skipButton = document.querySelector(
'.ytp-ad-skip-button, .ytp-skip-ad-button, [aria-label*="Skip"]'
);
const adOverlay = document.querySelector('.ytp-ad-player-overlay, .video-ads');
if (adOverlay && video) {
if (!isAdPlaying) {
originalVolume = video.volume;
isAdPlaying = true;
}
video.muted = true;
if (skipButton && skipButton.offsetParent !== null) {
skipButton.click();
}
} else if (isAdPlaying && video) {
video.muted = false;
video.volume = originalVolume;
isAdPlaying = false;
}
}
setInterval(detectAndMuteAds, 500);
const observer = new MutationObserver(detectAndMuteAds);
observer.observe(document.body, { childList: true, subtree: true });
The ad loads. The impression counts. The advertiser gets charged. The creator gets paid. What gets removed is the part where a pharmaceutical company screams into your headphones at full volume while you're trying to listen to something.
From YouTube's server, this looks like a user who muted the player and clicked skip very quickly. Rude. Efficient. Technically legal. YouTube built those controls. Mute Tube just uses them faster than a human would.
The dual approach — setInterval every 500ms plus a MutationObserver — exists because YouTube mutates the DOM in two different patterns depending on ad type. The interval catches some. The observer catches others. Running both covers the cases either one misses alone.
The Escalation
YouTube has not been passive about this. The escalation over the past few years tells you how seriously they take it.
Phase one was class name rotation. Change .ytp-ad-skip-button to something less guessable, break all the extensions that hardcoded that selector. This worked for a few days at a time. The wildcard selectors killed the strategy — if you match on the pattern instead of the exact name, rotating the name accomplishes nothing.
Phase two was ad detection detection. YouTube started watching for mechanical timing patterns — clicks arriving too fast, too precisely, at intervals that no human produces. The 500ms polling plus MutationObserver produces a more human-shaped timing signature than a pure interval would. Not perfect. Good enough.
Phase three is the one that actually hurts: server-side ad injection. Instead of inserting the ad through the DOM where client-side code can see it, YouTube stitches the ad directly into the video stream at the server level. From the browser's perspective, it is all one video. There is no ad-showing class to catch. There is no skip button to click. The ad just plays, and your extension is watching an empty DOM waiting for something that never arrives.
This is the round YouTube is winning right now. It is also the round that costs the most to run — encoding every stream twice, maintaining the injection infrastructure, adding latency to delivery. YouTube uses it selectively, which is why Mute Tube still works most of the time.
When it stops working, the repo gets updated. That is how it has always gone.
Read It Before You Trust It
Any browser extension has access to everything your browser touches on the matched domains. For YouTube, that is a lot. The right response to that is not faith — it is inspection.
git clone https://github.com/ghostintheprompt/mute-tube
cd mute-tube
Load it unpacked in chrome://extensions with developer mode on, open src/content/content.js, and read it. The whole content script is under a hundred lines. There is nothing in there that should take more than ten minutes to verify. If you find something that surprises you, open an issue. That is what the repo is for.
This is the advantage of keeping it small. An extension this size can be audited in a sitting. Most of the tools people install without reading are orders of magnitude more complex. Trust the thing you can read.
