Disable autoplay

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

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

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Disable autoplay
// @namespace    https://www.androidacy.com/
// @version      2.6.0
// @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');
    };

    // Detect if a click/touch is on the media or its cover
    const isTrustedInteraction = (event, media) => {
        if (!event.isTrusted) return false;

        const mediaRect = media.getBoundingClientRect();
        const x = event.clientX;
        const y = event.clientY;

        // 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 mediaElements = document.querySelectorAll('video, audio');
        mediaElements.forEach(media => {
            if (isTrustedInteraction(event, media)) {
                mediaInteractionMap.set(media, true);
                media.play().catch(() => {});
            }
        });
    };

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

})();