RamiEjailat / Auto Unmute AutoPlay Videos

// ==UserScript==
// @name         Auto Unmute AutoPlay Videos
// @description  Automatically unmutes videos that autoplay on page load, ignoring hidden ones (waits for user interaction to prevent autoplay blocking).
// @author       Rami S Ejailat
// @copyright    RamiEjailat (https://openuserjs.org/users/RamiEjailat)
// @updateURL    https://openuserjs.org/meta/RamiEjailat/Auto_Unmute_AutoPlay_Videos.meta.js
// @downloadURL  https://openuserjs.org/install/RamiEjailat/Auto_Unmute_AutoPlay_Videos.user.js
// @license      MIT
// @version      20260522
// @namespace    http://tampermonkey.net/
// @match        *://*/*
// @grant        none
// @icon         https://cdn-icons-png.flaticon.com/64/4406/4406124.png
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Flag to track if the browser's autoplay policy has been satisfied
    let userHasInteracted = false;

    // Helper function to determine if a video is actually visible to the user
    const isVisible = (el) => {
        const rect = el.getBoundingClientRect();
        const style = window.getComputedStyle(el);
        return rect.width > 10 &&
            rect.height > 10 &&
            style.display !== 'none' &&
            style.visibility !== 'hidden' &&
            style.opacity !== '0';
    };

    // Function to unmute and reset volume ONLY if visible AND user has interacted
    const unmuteVideo = (video) => {
        if (!userHasInteracted) return; // Prevent breaking the video before interaction

        if (isVisible(video)) {
            if (video.muted || video.volume === 0) {
                video.muted = false;
                if (video.volume === 0) video.volume = 1.0;
            }
        }
    };

    // Unlocks audio and unmutes existing videos upon first interaction
    const handleInteraction = () => {
        if (userHasInteracted) return;
        userHasInteracted = true;

        // Run through all videos and unmute them now that we are allowed to
        document.querySelectorAll('video').forEach(video => {
            if (!video.paused || video.autoplay) {
                unmuteVideo(video);
            }
        });

        // Clean up listeners once unlocked
        ['click', 'pointerdown', 'keydown'].forEach(evt => {
            document.removeEventListener(evt, handleInteraction, true);
        });
    };

    // Listen for the first user interaction to satisfy Autoplay Policy
    ['click', 'pointerdown', 'keydown'].forEach(evt => {
        document.addEventListener(evt, handleInteraction, { capture: true, once: true });
    });

    // Intercept videos as soon as they trigger a 'play' event
    document.addEventListener('play', (event) => {
        if (event.target && event.target.tagName === 'VIDEO') {
            unmuteVideo(event.target);
        }
    }, true);

    // Check videos already present when the script injects
    const checkExistingVideos = () => {
        document.querySelectorAll('video').forEach(video => {
            if (!video.paused || video.autoplay) {
                unmuteVideo(video);
            }
        });
    };

    window.addEventListener('DOMContentLoaded', checkExistingVideos);
    window.addEventListener('load', checkExistingVideos);
    checkExistingVideos();

    // Watch for dynamically added videos (Single Page Applications like React/Vue)
    const observer = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                // If the added node is a video
                if (node.tagName === 'VIDEO') {
                    if (node.autoplay || !node.paused) unmuteVideo(node);
                }
                // If the added node contains videos inside it
                else if (node.querySelectorAll) {
                    node.querySelectorAll('video').forEach(video => {
                        if (video.autoplay || !video.paused) unmuteVideo(video);
                    });
                }
            });
        });
    });

    // Start observing the document body for injected elements
    if (document.body) {
        observer.observe(document.body, { childList: true, subtree: true });
    } else {
        // Fallback if body isn't parsed yet
        window.addEventListener('DOMContentLoaded', () => {
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }
})();