Disable autoplay

Block autoplay before user interaction on most websites

目前為 2024-09-15 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Disable autoplay
// @namespace    https://www.androidacy.com/
// @version      2.0.0
// @description  Block autoplay before user interaction on most websites
// @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';

    // Generate a random 4-8 character string for tracking processed elements
    const generateRandomString = () => {
        const length = Math.floor(Math.random() * 5) + 4; // 4-8 characters
        const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        let result = '';
        for (let i = 0; i < length; i++) {
            result += chars.charAt(Math.floor(Math.random() * chars.length));
        }
        return `__disableAutoplay_${result}`;
    };

    const processedProp = generateRandomString();
    const allowedToPlay = new WeakSet();
    const mediaTags = ['video', 'audio'];

    function debugLog(...args) {
        console.debug('[DisableAutoplay]', ...args);
    }

    function warnLog(...args) {
        console.warn('[DisableAutoplay]', ...args);
    }

    function disableAutoplay(media) {
        if (media[processedProp]) {
            debugLog('Already processed media element:', media);
            return;
        }
        media[processedProp] = true;

        debugLog('Processing media element:', media);

        // Remove autoplay attribute if present
        if (media.hasAttribute('autoplay')) {
            media.removeAttribute('autoplay');
            debugLog('Removed autoplay attribute from media:', media);
        }

        // Pause the media if it's already playing
        if (!media.paused) {
            media.pause();
            debugLog('Paused media element:', media);
        }

        // Store the original play method
        const originalPlay = media.play;

        // Override the play method to control playback
        media.play = function(...args) {
            if (allowedToPlay.has(media)) {
                debugLog('Playing media element:', media);
                return originalPlay.apply(media, args);
            } else {
                warnLog('Autoplay blocked for media element:', media);
                return Promise.reject(new Error('Autoplay is disabled by a userscript.'));
            }
        };

        /**
         * Enables playback when a trusted user interaction is detected
         * @param {Event} event - The user interaction event
         */
        const enablePlayback = (event) => {
            if (!event.isTrusted) {
                warnLog('Ignored untrusted event:', event);
                return;
            }

            debugLog('User interaction detected:', event.type, 'on', event.target);
            allowedToPlay.add(media);
            media.play().catch(err => warnLog('Error playing media after user interaction:', err, media));

            // Remove event listeners after enabling playback
            media.removeEventListener('click', enablePlayback);
            media.removeEventListener('touchstart', enablePlayback);
            removeCoverListeners(media, enablePlayback);
        };

        // Add event listeners to media element without passive listeners
        media.addEventListener('click', enablePlayback, { once: true, passive: false });
        media.addEventListener('touchstart', enablePlayback, { once: true, passive: false });
        debugLog('Added click and touchstart event listeners to media element:', media);

        // Add event listeners to associated cover elements
        addCoverListeners(media, enablePlayback);
    }

    /**
     * Adds event listeners to cover elements associated with the media
     * @param {HTMLMediaElement} media - The media element
     * @param {Function} handler - The event handler to attach
     */
    function addCoverListeners(media, handler) {
        const covers = findCoverElements(media);
        covers.forEach(cover => {
            cover.addEventListener('click', handler, { once: true, passive: false });
            cover.addEventListener('touchstart', handler, { once: true, passive: false });
            debugLog('Added event listeners to cover element:', cover);
        });
    }

    /**
     * Removes event listeners from cover elements after playback is enabled
     * @param {HTMLMediaElement} media - The media element
     * @param {Function} handler - The event handler to remove
     */
    function removeCoverListeners(media, handler) {
        const covers = findCoverElements(media);
        covers.forEach(cover => {
            cover.removeEventListener('click', handler);
            cover.removeEventListener('touchstart', handler);
            debugLog('Removed event listeners from cover element:', cover);
        });
    }

    /**
     * Finds cover elements associated with a media element by analyzing computed styles
     * @param {HTMLMediaElement} media - The media element
     * @returns {HTMLElement[]} - Array of cover elements
     */
    function findCoverElements(media) {
        const covers = [];
        const mediaRect = media.getBoundingClientRect();
        const parent = media.parentElement;

        if (!parent) return covers;

        // Iterate through parent's children to find overlapping elements
        Array.from(parent.children).forEach(sibling => {
            if (sibling === media) return;

            const style = window.getComputedStyle(sibling);
            const position = style.position;
            const display = style.display;
            const visibility = style.visibility;
            const pointerEvents = style.pointerEvents;

            // Skip elements that are not visible or interactable
            if (display === 'none' || visibility === 'hidden' || pointerEvents === 'none') return;

            // Consider elements with certain positioning
            if (!['absolute', 'fixed', 'relative'].includes(position)) return;

            const siblingRect = sibling.getBoundingClientRect();

            // Check if sibling overlaps significantly with media
            if (isOverlapping(mediaRect, siblingRect)) {
                covers.push(sibling);
            }
        });

        return covers;
    }

    /**
     * Determines if two rectangles overlap by a certain threshold
     * @param {DOMRect} rect1 - First rectangle
     * @param {DOMRect} rect2 - Second rectangle
     * @returns {boolean} - True if overlapping sufficiently, else false
     */
    function isOverlapping(rect1, rect2) {
        const threshold = 0.3; // 30% overlap

        const intersection = {
            left: Math.max(rect1.left, rect2.left),
            right: Math.min(rect1.right, rect2.right),
            top: Math.max(rect1.top, rect2.top),
            bottom: Math.min(rect1.bottom, rect2.bottom)
        };

        const width = intersection.right - intersection.left;
        const height = intersection.bottom - intersection.top;

        if (width <= 0 || height <= 0) return false;

        const areaIntersection = width * height;
        const areaMedia = rect1.width * rect1.height;

        return (areaIntersection / areaMedia) >= threshold;
    }

    /**
     * Processes all existing media elements on the page
     */
    function processMediaElements() {
        mediaTags.forEach(tag => {
            document.querySelectorAll(tag).forEach(media => {
                disableAutoplay(media);
            });
        });
    }

    /**
     * Observes the DOM for any new media elements and processes them
     */
    function observeMedia() {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType !== Node.ELEMENT_NODE) return;

                    mediaTags.forEach(tag => {
                        // Direct match
                        if (node.matches(tag)) {
                            debugLog('New media element added:', node);
                            disableAutoplay(node);
                        }

                        // Nested media elements
                        node.querySelectorAll(tag).forEach(media => {
                            debugLog('New nested media element added:', media);
                            disableAutoplay(media);
                        });
                    });
                });
            });
        });

        observer.observe(document.documentElement, { childList: true, subtree: true });
        debugLog('Started observing DOM for new media elements');
    }

    /**
     * Initializes the userscript by processing existing media elements and setting up observers
     */
    function init() {
        debugLog('Initializing Disable autoplay userscript');
        processMediaElements();
        observeMedia();
    }

    // Run initialization at document-start
    init();

    // Also process media elements at DOMContentLoaded to catch any late-loaded media
    document.addEventListener('DOMContentLoaded', () => {
        debugLog('DOMContentLoaded event fired');
        processMediaElements();
    });

})();