Greasy Fork is available in English.

Disable autoplay

Block autoplay before user interaction on most websites, tracking each media element separately

// ==UserScript==
// @name         Disable autoplay
// @namespace    https://www.androidacy.com/
// @version      2.6.1
// @description  Block autoplay before user interaction on most websites, tracking each media element separately
// @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-end
// ==/UserScript==

(() => {
    'use strict';

    // WeakMap to track user interaction for each media element
    const mediaInteractionMap = new WeakMap();

    // Helper function to pause all media elements
    const pauseAllMedia = () => {
        document.querySelectorAll('video, audio').forEach(media => {
            media.pause();
        });
    };

    // Initial pause of any playing media
    pauseAllMedia();

    // Remove autoplay attributes from existing media elements
    const removeAutoplay = (media) => {
        media.removeAttribute('autoplay');
    };

    /**
     * Extracts the clientX and clientY coordinates from the event based on its type.
     * @param {Event} event - The event object.
     * @returns {{x: number, y: number} | null} - The extracted coordinates or null if not available.
     */
    const extractCoordinates = (event) => {
        if (event.type === 'click') {
            const { clientX: x, clientY: y } = event;
            return isFinite(x) && isFinite(y) ? { x, y } : null;
        } else if (event.type === 'touchstart') {
            if (event.touches.length > 0) {
                const touch = event.touches[0];
                const { clientX: x, clientY: y } = touch;
                return isFinite(x) && isFinite(y) ? { x, y } : null;
            }
        }
        return null;
    };

    /**
     * Detects if the interaction is trusted for the given media element.
     * @param {Event} event - The user interaction event.
     * @param {HTMLElement} media - The media element to check against.
     * @param {number} x - The x-coordinate of the interaction.
     * @param {number} y - The y-coordinate of the interaction.
     * @returns {boolean} - True if the interaction is trusted, else false.
     */
    const isTrustedInteraction = (event, media, x, y) => {
        if (!event.isTrusted) return false;

        const mediaRect = media.getBoundingClientRect();

        // Check if the interaction is within the media bounds
        if (
            x >= mediaRect.left &&
            x <= mediaRect.right &&
            y >= mediaRect.top &&
            y <= mediaRect.bottom
        ) {
            return true;
        }

        // Check if within 64px of the sides
        const within64px =
            x >= mediaRect.left - 64 &&
            x <= mediaRect.right + 64 &&
            y >= mediaRect.top - 64 &&
            y <= mediaRect.bottom + 64;

        if (within64px) {
            return true;
        }

        // Additional check for overlapping elements (covers)
        const elementsAtPoint = document.elementsFromPoint(x, y);
        for (const el of elementsAtPoint) {
            if (el !== media && media.contains(el)) {
                return true;
            }
        }

        return false;
    };

    // Override play method to block autoplay
    const overridePlay = (media) => {
        const originalPlay = media.play.bind(media);
        media.play = async () => {
            if (mediaInteractionMap.get(media)) {
                try {
                    await originalPlay();
                } catch (e) {
                    console.error('Playback failed:', e);
                }
            } else {
                media.pause();
                console.log('Autoplay blocked for:', media);
                return Promise.reject(new Error('Autoplay is blocked.'));
            }
        };
    };

    // Handle user interactions to allow playback for specific media elements
    const handleUserInteraction = (event) => {
        const coordinates = extractCoordinates(event);
        if (!coordinates) return;

        const { x, y } = coordinates;

        // Ensure coordinates are finite numbers
        if (!isFinite(x) || !isFinite(y)) return;

        const mediaElements = document.querySelectorAll('video, audio');
        mediaElements.forEach(media => {
            if (isTrustedInteraction(event, media, x, y)) {
                if (!mediaInteractionMap.get(media)) {
                    mediaInteractionMap.set(media, true);
                    media.play().catch(() => {});
                    console.log('User interacted with media:', media);
                }
            }
        });
    };

    // Add event listeners for user interactions
    window.addEventListener('click', handleUserInteraction, true);
    window.addEventListener('touchstart', handleUserInteraction, true);

    // Process a media element: remove autoplay and override play
    const processMediaElement = (media) => {
        removeAutoplay(media);
        overridePlay(media);

        // Initialize interaction state as false
        if (!mediaInteractionMap.has(media)) {
            mediaInteractionMap.set(media, false);
        }

        // Listen for play events and pause if playback is not allowed
        media.addEventListener('play', () => {
            if (!mediaInteractionMap.get(media)) {
                media.pause();
                console.log('Playback paused for:', media, 'due to no user interaction.');
            }
        });
    };

    // Initial processing of existing media elements
    document.querySelectorAll('video, audio').forEach(processMediaElement);

    // Observe for dynamically added media elements
    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.matches('video, audio')) {
                        processMediaElement(node);
                    }
                    // Also check within the subtree
                    node.querySelectorAll && node.querySelectorAll('video, audio').forEach(processMediaElement);
                }
            });
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();