Disable autoplay

Block autoplay before user interaction on most websites

当前为 2024-09-15 提交的版本,查看 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Disable autoplay
// @namespace    https://www.androidacy.com/
// @version      2.2.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-end
// ==/UserScript==

(function() {
    'use strict';

    // Generate a unique property name for tracking processed elements
    const processedProp = `__disableAutoplay_${Math.random().toString(36).substr(2, 8)}`;

    // WeakSet to track media elements allowed to play
    const allowedToPlay = new WeakSet();

    // List of media tags to target
    const mediaTags = ['video', 'audio'];

    /**
     * Logs debug messages with a consistent prefix
     * @param  {...any} args - Messages or objects to log
     */
    function debugLog(...args) {
        console.debug('[DisableAutoplay]', ...args);
    }

    /**
     * Logs warning messages with a consistent prefix
     * @param  {...any} args - Messages or objects to warn
     */
    function warnLog(...args) {
        console.warn('[DisableAutoplay]', ...args);
    }

    /**
     * Disables autoplay on a given media element
     * @param {HTMLMediaElement} media - The media element to process
     */
    function disableAutoplay(media) {
        // Check if the media element has already been processed
        if (media[processedProp]) {
            debugLog('Already processed media element:', media);
            return;
        }

        // Mark the media element as processed
        Object.defineProperty(media, processedProp, {
            value: true,
            writable: false,
            configurable: false,
            enumerable: false
        });

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

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

        // Pause the media if it's currently 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 the 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.body, { 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-end
    init();

})();