Skip to content

Instantly share code, notes, and snippets.

@ochafik
Last active October 27, 2025 15:45
Show Gist options
  • Select an option

  • Save ochafik/b9280a381d31f976757fb1aa600d5bc4 to your computer and use it in GitHub Desktop.

Select an option

Save ochafik/b9280a381d31f976757fb1aa600d5bc4 to your computer and use it in GitHub Desktop.

Ad Skipper Chrome Extension

Published here: https://gist.github.com/ochafik/b9280a381d31f976757fb1aa600d5bc4

A Chrome extension (Manifest V3) that automatically handles ads on Disney+, YouTube and Amazon Prime Video by:

  • Muting all video elements during ads
  • Hiding video (makes invisible) during ads
  • Speeding up playback to 16x during ads (makes them finish faster)
  • Auto-clicking skip buttons (YouTube skippable ads, Disney+ promos & Amazon Prime ads)
  • Announcing via text-to-speech when ads start/end and skip buttons are clicked
  • Pausing video after ads end, waiting 10 seconds, then resuming

Installation

  1. Click Download ZIP at the top-right of this screen. Unzip the content somewhere non-temporary.
  2. Open Chrome and navigate to chrome://extensions/
  3. Enable "Developer mode" (toggle in the top-right corner)
  4. Click "Load unpacked"
  5. Select the directory where you unzipped this repo's content.

Files

  • manifest.json: Extension configuration (Manifest V3)
  • injected.js: Main script that detects ads and controls video playback

How It Works

  1. Runs in MAIN world to access video elements directly
  2. Detects ads using platform-specific selectors:
    • Disney+: Monitors .btm-media-player for interstitial-ad-playing class
    • YouTube: Checks for [aria-label="Sponsored"] elements
    • Amazon Prime: Detects .atvwebplayersdk-ad-timer element
  3. When ads detected:
    • Saves original settings for all video elements in a WeakMap
    • Mutes all videos
    • Sets playback rate to 16x (maximum speed)
    • Hides videos (visibility: hidden)
    • Announces "Commercials muted" (debounced)
  4. When ads end:
    • Restores all videos that were modified (based on WeakMap)
    • Pauses video
    • Announces "Commercials are over" (debounced)
    • Waits 10 seconds then resumes playback
  5. Continuously monitors for skip buttons:
    • YouTube: .ytp-skip-ad-button (skippable video ads)
    • Disney+: <promo-overlay> shadow DOM for promo skip buttons
    • Amazon Prime: Generic skip button selectors
  6. Auto-clicks skip buttons and announces "Ad skipped"

Features

  • Multi-video handling: Tracks and restores settings for all video elements using WeakMap
  • Debouncing: Prevents multiple announcements during ad pod transitions (15 second threshold)
  • Safety restore: Automatically restores videos if still transformed after ads end
  • Page visibility tracking: Won't announce if user navigates away
  • Shadow DOM support: Finds and clicks skip buttons inside shadow DOMs

Permissions

None required! Uses Web Speech API for text-to-speech (no chrome.tts permission needed).

Testing

  1. Load the extension in Chrome
  2. Navigate to Disney+, YouTube or Amazon Prime Video and start watching content with ads
  3. Ads should be invisible, muted, and play at 16x speed
  4. Skip buttons should be automatically clicked when they appear
  5. Main content should be properly restored after ads
  6. Platform-specific ad detection should work on all supported services

Troubleshooting

  • Check the browser console for debug messages prefixed with [Ad Skipper]
  • Video should log "X video(s) muted, hidden, and sped up" when ads start
  • If video stays hidden, check for "Safety restore" message in console
  • On YouTube, verify the sponsored label appears with document.querySelector('[aria-label="Sponsored"]')
  • On Amazon Prime, verify the ad timer appears with document.querySelector('.atvwebplayersdk-ad-timer')
// Ad Skipper - Injected Script (runs in MAIN world)
// Mutes, hides, speeds up and auto-skips ads on Disney+ and YouTube
// This script has access to the page's video elements
(function() {
'use strict';
let adBadgeVisible = false;
let observer = null;
let adEndTimeout = null;
let adEndDebounceTimeout = null;
let lastAdEndTime = 0;
let lastAdStartTime = 0;
let isPageVisible = !document.hidden;
let safetyUnmuteTimeout = null;
let lastPromoSkipTime = 0;
let promoObserver = null;
// Map to track original settings for each video element
const videoSettings = new WeakMap();
// Get all video elements
function getAllVideos() {
return Array.from(document.querySelectorAll('video'));
}
// Find the primary video element (for backward compatibility)
function getVideo() {
return document.querySelector('video');
}
// Save original settings for a video element
function saveVideoSettings(video) {
if (!videoSettings.has(video)) {
videoSettings.set(video, {
volume: video.volume,
playbackRate: video.playbackRate,
opacity: video.style.opacity
});
}
}
// Centralized TTS function
function speak(text) {
if ('speechSynthesis' in window) {
// Cancel any ongoing speech to prevent queueing
speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
speechSynthesis.speak(utterance);
console.log('[Ad Skipper] TTS:', text);
}
}
// Announce commercials muted
function announceMuted() {
const now = Date.now();
// Only announce if it's been more than 15 seconds since last mute announcement
if (now - lastAdStartTime > 15000) {
lastAdStartTime = now;
speak('Muting commercials');
} else {
console.log('[Ad Skipper] Debounced mute announcement - likely pod transition');
}
}
// Mute ALL videos and apply ad transformations
function muteVideo() {
const videos = getAllVideos();
let anyMuted = false;
videos.forEach(video => {
// Always save settings before modifying (even if already muted)
saveVideoSettings(video);
// Apply ad transformations to ALL videos
video.muted = true;
video.playbackRate = 16.0; // Maximum playback speed (browsers cap around 16x)
video.style.opacity = '0'; // Make video invisible without affecting layout
anyMuted = true;
});
if (anyMuted) {
console.log('[Ad Skipper]', videos.length, 'video(s) muted, hidden, and sped up - ad detected');
announceMuted();
}
}
// Restore ONLY videos we modified (ones in the WeakMap)
function unmuteVideo() {
const videos = getAllVideos();
let restoredCount = 0;
videos.forEach(video => {
// Only restore videos that are in our WeakMap (ones we modified)
const settings = videoSettings.get(video)
if (settings) {
videoSettings.delete(video); // Remove from map after restoring
// Restore original settings
video.muted = false;
video.volume = settings.volume;
video.playbackRate = settings.playbackRate;
video.style.opacity = settings.opacity;
restoredCount++;
}
});
if (restoredCount > 0) {
console.log('[Ad Skipper]', restoredCount, 'video(s) unmuted and restored');
}
}
// Pause the video
function pauseVideo() {
const video = getVideo();
if (video && !video.paused) {
video.pause();
console.log('[Ad Skipper] Video paused');
}
}
// Play the video
function playVideo() {
const video = getVideo();
if (video && video.paused) {
video.play();
console.log('[Ad Skipper] Video playing');
}
}
// Announce commercials are over
function announceAdEnd() {
speak('Commercials are over');
}
// Handle ad badge visibility change
function handleAdBadgeChange(isVisible) {
if (isVisible && !adBadgeVisible) {
// Ad just became visible - mute the video
adBadgeVisible = true;
muteVideo();
// Clear debounce timeout since we're back in ad mode
if (adEndDebounceTimeout) {
clearTimeout(adEndDebounceTimeout);
adEndDebounceTimeout = null;
}
if (adEndTimeout) {
clearTimeout(adEndTimeout);
adEndTimeout = null;
}
} else if (!isVisible && adBadgeVisible) {
// Ad just became invisible - but debounce to avoid multiple ad breaks
adBadgeVisible = false;
const now = Date.now();
// Clear existing debounce
if (adEndDebounceTimeout) {
clearTimeout(adEndDebounceTimeout);
}
// Wait 500ms to see if another ad starts (debounce)
adEndDebounceTimeout = setTimeout(() => {
// Check if we're still in non-ad state (another ad might have started)
if (adBadgeVisible) {
console.log('[Ad Skipper] Debounce cancelled - new ad started');
return;
}
// Only trigger ad end behavior if:
// 1. It's been more than 15 seconds since last ad end (prevents multiple announcements)
// 2. Page is still visible (not navigating away)
if (now - lastAdEndTime > 15000 && isPageVisible) {
lastAdEndTime = now;
pauseVideo();
unmuteVideo();
announceAdEnd();
// Clear any existing timeout
if (adEndTimeout) {
clearTimeout(adEndTimeout);
}
// Wait 2 seconds then play
adEndTimeout = setTimeout(() => {
playVideo();
console.log('[Ad Skipper] Resuming playback after 2 seconds');
}, 2000);
} else {
console.log('[Ad Skipper] Debounced ad end - likely pod transition or page hidden');
// Still unmute even if we skip announcement
unmuteVideo();
}
}, 500);
}
}
// Auto-click skip button if available
function clickSkipButton() {
// Look for skip button - try various selectors in regular DOM
const skipSelectors = [
'.ytp-skip-ad-button', // YouTube skip ad button (most specific)
'button[aria-label*="skip" i]',
'button[class*="skip" i]',
'.skip-button',
'[data-testid*="skip" i]'
];
for (const selector of skipSelectors) {
const skipBtn = document.querySelector(selector);
if (skipBtn && skipBtn.offsetParent !== null) {
skipBtn.click();
console.log('[Ad Skipper] Clicked skip button:', selector);
announceSkipped();
return true;
}
}
// Also check inside shadow DOMs (e.g., promo-overlay)
const shadowHosts = ['promo-overlay', 'ad-badge-overlay'];
for (const hostSelector of shadowHosts) {
const host = document.querySelector(hostSelector);
if (host && host.shadowRoot) {
for (const selector of skipSelectors) {
const skipBtn = host.shadowRoot.querySelector(selector);
if (skipBtn && skipBtn.offsetParent !== null) {
skipBtn.click();
console.log('[Ad Skipper] Clicked skip button in shadow DOM:', hostSelector);
announceSkipped();
return true;
}
}
// Also try generic button with "SKIP" text
const buttons = host.shadowRoot.querySelectorAll('button');
for (const btn of buttons) {
if (btn.textContent && btn.textContent.trim().toUpperCase().includes('SKIP')) {
if (btn.offsetParent !== null) {
btn.click();
console.log('[Ad Skipper] Clicked SKIP button by text in shadow DOM:', hostSelector);
announceSkipped();
return true;
}
}
}
}
}
return false;
}
// Announce ad/promotion skipped
function announceSkipped() {
speak('Ad skipped');
}
// Ensure ALL videos stay muted during ads (even if video elements change)
function ensureAdMuted() {
const videos = getAllVideos();
let anyChanged = false;
videos.forEach(video => {
// Save settings if not already saved
saveVideoSettings(video);
// Always enforce ad transformations
if (!video.muted) {
video.muted = true;
anyChanged = true;
}
if (video.playbackRate !== 16.0) {
video.playbackRate = 16.0;
anyChanged = true;
}
if (video.style.opacity !== '0') {
video.style.opacity = '0';
anyChanged = true;
}
});
if (anyChanged) {
console.log('[Ad Skipper] Re-applied ad transformations to', videos.length, 'video(s)');
}
}
// Detect if ad is playing (platform-specific)
function detectAd() {
// Disney+ detection
const disneyPlayer = document.querySelector('.btm-media-player');
if (disneyPlayer && disneyPlayer.classList.contains('interstitial-ad-playing')) {
return true;
}
// YouTube detection - only on watch pages (e.g., /watch?v=...)
if (window.location.hostname.includes('youtube.com') &&
window.location.pathname === '/watch') {
const youtubeSponsored = document.querySelector('[aria-label="Sponsored"]');
if (youtubeSponsored) {
return true;
}
}
// Amazon Prime Video detection
if (window.location.hostname.includes('amazon.') ||
window.location.hostname.includes('primevideo.com')) {
const primeAdTimer = document.querySelector('.atvwebplayersdk-ad-timer');
if (primeAdTimer) {
return true;
}
}
return false;
}
// Check if ad is playing
function checkAdBadge() {
const isAdPlaying = detectAd();
// Debug logging
if (isAdPlaying !== adBadgeVisible) {
console.log('[Ad Skipper] Ad state changed:', isAdPlaying ? 'AD DETECTED' : 'NO AD');
}
// If ad is playing, ensure ALL video elements stay muted (handles video swapping)
if (isAdPlaying) {
ensureAdMuted();
clickSkipButton();
}
// Safety net: restore videos we modified if no ad is playing
if (!isAdPlaying) {
const videos = getAllVideos();
// Check if ANY video we modified still needs restoring
const needsRestore = videos.some(v => videoSettings.has(v));
if (needsRestore) {
// Clear any existing safety unmute timeout
if (safetyUnmuteTimeout) {
clearTimeout(safetyUnmuteTimeout);
}
// Wait 500ms before safety unmuting (in case next ad is loading)
safetyUnmuteTimeout = setTimeout(() => {
// Double-check we're still not in ad mode
const stillNoAd = !detectAd();
if (stillNoAd) {
const vids = getAllVideos();
const hasModifiedVideos = vids.some(v => videoSettings.has(v));
if (hasModifiedVideos) {
console.log('[Ad Skipper] Safety restore - no ad detected but have modified videos');
unmuteVideo();
}
} else {
console.log('[Ad Skipper] Safety restore cancelled - ad started');
}
}, 500);
}
} else {
// Clear safety unmute timeout if ad is detected
if (safetyUnmuteTimeout) {
clearTimeout(safetyUnmuteTimeout);
safetyUnmuteTimeout = null;
}
}
handleAdBadgeChange(isAdPlaying);
}
// Setup mutation observer to watch for ad badge changes
function setupObserver() {
if (observer) {
observer.disconnect();
}
observer = new MutationObserver((mutations) => {
checkAdBadge();
});
// Watch the player element directly for class changes
const player = document.querySelector('.btm-media-player');
if (player) {
observer.observe(player, {
attributes: true,
attributeFilter: ['class']
});
console.log('[Ad Skipper] Observer watching player element for class changes');
} else {
// Fallback: observe the entire document
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'hidden']
});
console.log('[Ad Skipper] Observer setup on document.body (player not found yet)');
}
}
// Watch promo-overlay for skip button appearing
function watchPromoOverlay() {
const checkPromoSkip = () => {
const promo = document.querySelector('promo-overlay');
if (!promo || !promo.shadowRoot) return;
const buttons = promo.shadowRoot.querySelectorAll('button');
for (const btn of buttons) {
if (btn.textContent && btn.textContent.trim().toUpperCase().includes('SKIP')) {
// Check if button is visible
if (btn.offsetParent !== null) {
// Debounce: only click once every 5 seconds
const now = Date.now();
if (now - lastPromoSkipTime > 5000) {
lastPromoSkipTime = now;
btn.click();
console.log('[Ad Skipper] Clicked promo skip button');
announceSkipped();
}
}
}
}
};
// Set up observer on promo-overlay's shadow DOM if available
const setupPromoObserver = () => {
const promo = document.querySelector('promo-overlay');
if (promo && promo.shadowRoot) {
if (promoObserver) {
promoObserver.disconnect();
}
promoObserver = new MutationObserver(() => {
checkPromoSkip();
});
promoObserver.observe(promo.shadowRoot, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});
console.log('[Ad Skipper] Watching promo-overlay shadow DOM for skip button');
}
};
// Try to set up observer immediately
setupPromoObserver();
// Also periodically check (in case observer misses something)
setInterval(checkPromoSkip, 500);
// Re-check observer setup periodically (in case shadow DOM appears later)
setInterval(setupPromoObserver, 5000);
}
// Initialize the extension
function initialize() {
console.log('[Ad Skipper] Initializing (injected script)...');
// Track page visibility to prevent announcements when navigating away
document.addEventListener('visibilitychange', () => {
isPageVisible = !document.hidden;
console.log('[Ad Skipper] Page visibility changed:', isPageVisible);
});
// Wait for video element to be available
const checkForVideo = setInterval(() => {
if (getVideo()) {
clearInterval(checkForVideo);
console.log('[Ad Skipper] Video element found');
setupObserver();
checkAdBadge(); // Initial check
}
}, 1000);
// Also check periodically in case the observer misses something
setInterval(checkAdBadge, 2000);
// Start watching for promo overlays
watchPromoOverlay();
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();
{
"manifest_version": 3,
"name": "Ad Skipper",
"version": "1.0.0",
"description": "Automatically mutes, hides, speeds up and auto-skips ads on Disney+, YouTube and Amazon Prime Video",
"content_scripts": [
{
"matches": ["*://*.disneyplus.com/*", "*://*.youtube.com/*", "*://*.amazon.com/*", "*://*.amazon.co.uk/*", "*://*.primevideo.com/*"],
"js": ["injected.js"],
"run_at": "document_end",
"world": "MAIN"
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment