Ultimate Autoplay Blocker

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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 });
})();