Ultimate Autoplay Blocker

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 });
})();