Disable Autoplay

Block autoplay and require direct user interaction to play media, detecting covers via computed styles

נכון ליום 15-09-2024. ראה הגרסה האחרונה.

// ==UserScript==
// @name         Disable Autoplay
// @namespace    https://www.androidacy.com/
// @version      1.7.1
// @description  Block autoplay and require direct user interaction to play media, detecting covers via computed styles
// @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';

    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 (allowedToPlay.has(media)) return;

        debugLog('Processing media:', media);

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

        // Pause if playing
        if (!media.paused) {
            media.pause();
            debugLog('Paused media');
        }

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

        // Enable playback on trusted user interaction
        const enablePlayback = (event) => {
            if (!event.isTrusted) {
                warnLog('Untrusted event ignored:', event);
                return;
            }

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

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

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

        // Add event listeners to cover elements detected via computed styles
        addCoverListeners(media, enablePlayback);
    }

    function addCoverListeners(media, handler) {
        const covers = findCoverElements(media);
        covers.forEach(cover => {
            cover.addEventListener('click', handler, { once: true });
            cover.addEventListener('touchstart', handler, { once: true });
            debugLog('Added event listeners to cover:', cover);
        });
    }

    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:', cover);
        });
    }

    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;

            if (display === 'none' || visibility === 'hidden' || pointerEvents === 'none') return;

            if (position !== 'absolute' && position !== 'fixed' && position !== 'relative') return;

            const siblingRect = sibling.getBoundingClientRect();

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

        return covers;
    }

    function isOverlapping(rect1, rect2) {
        const threshold = 0.3; // Minimum overlap percentage

        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;
    }

    function processMediaElements() {
        mediaTags.forEach(tag => {
            document.querySelectorAll(tag).forEach(media => disableAutoplay(media));
        });
    }

    function observeMedia() {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType !== Node.ELEMENT_NODE) return;
                    mediaTags.forEach(tag => {
                        if (node.matches(tag)) {
                            debugLog('New media added:', node);
                            disableAutoplay(node);
                        }
                        node.querySelectorAll(tag).forEach(media => {
                            debugLog('New nested media added:', media);
                            disableAutoplay(media);
                        });
                    });
                });
            });
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        debugLog('Started observing DOM for new media elements');
    }

    function init() {
        debugLog('Initializing DisableAutoplay userscript');
        processMediaElements();
        observeMedia();
    }

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

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

})();