Ultimate Autoplay Blocker

The last userscript you'll ever need to disable autoplay videos on news sites and elsewhere

// ==UserScript==
// @name         Ultimate Autoplay Blocker
// @namespace    https://www.androidacy.com/
// @version      3.1.0
// @description  The last userscript you'll ever need to disable autoplay videos on news sites and elsewhere
// @author       Androidacy
// @include      *
// @icon         https://www.androidacy.com/wp-content/uploads/cropped-cropped-cropped-cropped-New-Project-32-69C2A87-1-192x192.jpg
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // WeakMap to track user interaction status for each media element
    const mediaInteractionMap = new WeakMap();
    
    // Store original play method
    const originalPlay = HTMLMediaElement.prototype.play;
    
    // Track initialization status
    let initialized = false;
    
    // ID for interval timer
    let checkIntervalId = null;

    // Helper: Extract coordinates from event
    const getCoordinates = (event) => {
        if (!event) return null;
        
        try {
            if (event.type.startsWith('mouse')) {
                return { x: event.clientX, y: event.clientY };
            } else if (event.type === 'touchstart' || event.type === 'touchmove' || event.type === 'touchend') {
                // Handle both active touches and changed touches (for touchend)
                const touch = event.touches?.[0] || event.changedTouches?.[0];
                return touch ? { x: touch.clientX, y: touch.clientY } : null;
            }
        } catch (e) {
            // Silently fail if we can't extract coordinates
        }
        return null;
    };

    // Helper: Check if interaction is related to media element
    const isMediaInteraction = (event, media) => {
        if (!event?.isTrusted || !media) return false;
        
        try {
            // For keyboard events on the media itself
            if ((event.type === 'keydown' || event.type === 'keyup') && 
               (document.activeElement === media || media.contains(document.activeElement))) {
                return ['Space', ' ', 'Enter', 'k', 'K'].includes(event.key);
            }
            
            const coords = getCoordinates(event);
            if (!coords) return false;
            
            const { x, y } = coords;
            const rect = media.getBoundingClientRect();
            
            // Direct interaction with the media element
            if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
                return true;
            }
            
            // Check if interaction is near the media (for controls that appear outside)
            const proximity = 64; // px
            if (x >= rect.left - proximity && x <= rect.right + proximity && 
                y >= rect.top - proximity && y <= rect.bottom + proximity) {
                
                // Check elements at interaction position
                try {
                    const elementsAtPoint = document.elementsFromPoint(x, y);
                    if (!elementsAtPoint || elementsAtPoint.length === 0) return false;
                    
                    for (const el of elementsAtPoint) {
                        // Check if element is or contains media controls
                        if (el === media || media.contains(el) || el.contains(media)) {
                            return true;
                        }
                        
                        // Check for common control elements
                        const tagName = el.tagName?.toLowerCase();
                        const role = el.getAttribute('role')?.toLowerCase();
                        const elClass = (el.className || '').toString().toLowerCase();
                        const elId = (el.id || '').toString().toLowerCase();
                        const ariaLabel = el.getAttribute('aria-label')?.toLowerCase();
                        
                        // Check for play buttons and controls
                        if (tagName === 'button' || role === 'button' || 
                            elClass.includes('play') || elId.includes('play') ||
                            elClass.includes('control') || elId.includes('control') ||
                            (ariaLabel && (ariaLabel.includes('play') || ariaLabel.includes('start'))) ||
                            el.onclick || el.parentElement?.onclick) {
                            return true;
                        }
                        
                        // Check for common player UI patterns
                        const playerPatterns = ['player', 'video', 'media', 'youtube', 'vimeo', 'jwplayer'];
                        for (const pattern of playerPatterns) {
                            if (elClass.includes(pattern) || elId.includes(pattern)) {
                                return true;
                            }
                        }
                    }
                } catch (e) {
                    // If elementsFromPoint fails, fall back to less precise detection
                    return document.activeElement === media;
                }
            }
        } catch (e) {
            // If anything fails, be conservative
            return false;
        }
        
        return false;
    };

    // Process a media element
    const processMedia = (media) => {
        if (!media || !(media instanceof HTMLMediaElement)) return;
        
        try {
            // Set initial interaction state if not already set
            if (!mediaInteractionMap.has(media)) {
                mediaInteractionMap.set(media, false);
                
                // Remove autoplay attribute
                if (media.hasAttribute('autoplay')) {
                    media.removeAttribute('autoplay');
                }
                
                // Disable autoplay property
                if (media.autoplay) {
                    media.autoplay = false;
                }
                
                // Listen for play events to pause if autoplay attempted
                media.addEventListener('play', function(e) {
                    if (!mediaInteractionMap.get(this)) {
                        this.pause();
                    }
                }, true);
                
                // Ensure new sources don't trigger autoplay
                media.addEventListener('loadedmetadata', function() {
                    if (!mediaInteractionMap.get(this)) {
                        this.pause();
                    }
                }, true);
                
                // Pause if it's already playing
                if (!media.paused && !mediaInteractionMap.get(media)) {
                    media.pause();
                }
            }
        } catch (e) {
            // Ignore errors in processing
        }
    };

    // Handle user interactions
    const handleUserInteraction = (event) => {
        if (!event?.isTrusted) return;
        
        try {
            document.querySelectorAll('video, audio').forEach(media => {
                if (isMediaInteraction(event, media)) {
                    mediaInteractionMap.set(media, true);
                }
            });
        } catch (e) {
            // Ignore errors in event handling
        }
    };

    // Override HTMLMediaElement.prototype.play
    const overridePlayMethod = () => {
        try {
            HTMLMediaElement.prototype.play = function() {
                try {
                    if (mediaInteractionMap.get(this)) {
                        return originalPlay.apply(this);
                    } else {
                        this.pause();
                        return Promise.reject(new DOMException('NotAllowedError', 'Autoplay blocked by userscript'));
                    }
                } catch (e) {
                    // If there's an error in our override, fall back to original behavior
                    return originalPlay.apply(this);
                }
            };
        } catch (e) {
            // If we can't override play, continue with other protections
        }
    };

    // Override setAttribute to block autoplay attribute
    const overrideSetAttribute = () => {
        try {
            const originalSetAttribute = Element.prototype.setAttribute;
            Element.prototype.setAttribute = function(name, value) {
                if (name === 'autoplay' && this instanceof HTMLMediaElement) {
                    return;
                }
                return originalSetAttribute.call(this, name, value);
            };
        } catch (e) {
            // If we can't override setAttribute, continue with other protections
        }
    };

    // Process all media elements
    const processAllMedia = () => {
        try {
            document.querySelectorAll('video, audio').forEach(processMedia);
        } catch (e) {
            // Ignore errors when processing all media
        }
    };

    // Set up mutation observer for dynamically added elements
    const setupObserver = () => {
        try {
            const observer = new MutationObserver(mutations => {
                let foundMedia = false;
                
                for (const mutation of mutations) {
                    // Check for added nodes
                    if (mutation.addedNodes.length > 0) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node instanceof HTMLMediaElement) {
                                    processMedia(node);
                                    foundMedia = true;
                                } else if (node.querySelectorAll) {
                                    const mediaElements = node.querySelectorAll('video, audio');
                                    if (mediaElements.length > 0) {
                                        foundMedia = true;
                                        mediaElements.forEach(processMedia);
                                    }
                                }
                            }
                        }
                    }
                    
                    // Check for attribute changes (autoplay)
                    if (mutation.type === 'attributes' && 
                        mutation.attributeName === 'autoplay' && 
                        mutation.target instanceof HTMLMediaElement) {
                        mutation.target.removeAttribute('autoplay');
                    }
                }
                
                // Only scan all media if we found some to avoid performance issues
                if (foundMedia) {
                    processAllMedia();
                }
            });
            
            observer.observe(document.documentElement || document, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['autoplay']
            });
        } catch (e) {
            // If observer setup fails, rely on interval checking instead
            ensureIntervalActive();
        }
    };

    // Add event listeners for user interactions
    const setupEventListeners = () => {
        try {
            // Desktop events
            ['click', 'mousedown'].forEach(eventType => {
                window.addEventListener(eventType, handleUserInteraction, true);
            });
            
            // Mobile events
            ['touchstart', 'touchend'].forEach(eventType => {
                window.addEventListener(eventType, handleUserInteraction, { 
                    capture: true,
                    passive: true 
                });
            });
            
            // Keyboard events
            ['keydown'].forEach(eventType => {
                window.addEventListener(eventType, handleUserInteraction, true);
            });
        } catch (e) {
            // If event setup fails, still continue with other protections
        }
    };

    // Pause all currently playing media
    const pauseAllMedia = () => {
        try {
            document.querySelectorAll('video, audio').forEach(media => {
                if (!mediaInteractionMap.get(media) && !media.paused) {
                    media.pause();
                }
            });
        } catch (e) {
            // Ignore errors when pausing
        }
    };
    
    // Ensure the interval is active
    const ensureIntervalActive = () => {
        if (!checkIntervalId) {
            checkIntervalId = setInterval(() => {
                processAllMedia();
                pauseAllMedia();
            }, 2000);
        }
    };

    // Initialize
    const initialize = () => {
        if (initialized) return;
        initialized = true;
        
        // Override methods to intercept JS autoplay
        overridePlayMethod();
        overrideSetAttribute();
        
        // Set up event listeners
        setupEventListeners();
        
        // Process existing media elements
        processAllMedia();
        
        // Pause any currently playing media
        pauseAllMedia();
        
        // Set up mutation observer for dynamically added elements
        setupObserver();
        
        // Ensure interval is active as a backup
        ensureIntervalActive();
    };

    // Initialize as early as possible
    if (document.readyState !== 'loading') {
        initialize();
    } else {
        document.addEventListener('DOMContentLoaded', initialize, { once: true });
    }
    
    // Backup initialization when window loads
    window.addEventListener('load', initialize, { once: true });
})();