Facebook Reels Enhancer

Enable controls on Facebook Reels videos, auto-unmute, and prevent pausing when switching tabs.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Facebook Reels Enhancer
// @namespace    UserScript
// @match        https://www.facebook.com/*
// @version      2.3
// @license      MIT
// @author       Pyrvox
// @description  Enable controls on Facebook Reels videos, auto-unmute, and prevent pausing when switching tabs.
// @icon         https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Wait for document to be ready before adding styles
    function addStyles() {
        if (!document.head) {
            setTimeout(addStyles, 10);
            return;
        }

        // Add CSS for better controls handling
        const style = document.createElement('style');
        style.textContent = `
            /* Disable overlay that blocks controls */
            .x1ey2m1c.x78zum5.xdt5ytf.xozqiw3.x17qophe.x13a6bvl.x10l6tqk.x1hkcv85,
            .x1ey2m1c.x78zum5.xdt5ytf.xozqiw3.x17qophe.x13a6bvl.x10l6tqk {
                pointer-events: none !important;
                opacity: 0.01 !important;
            }

            /* Make video controls always visible and clickable */
            video::-webkit-media-controls {
                opacity: 1 !important;
                visibility: visible !important;
                z-index: 99999 !important;
            }

            /* Make video container clickable */
            .x1ey2m1c.x78zum5.xdt5ytf.xozqiw3.x17qophe.x13a6bvl.x10l6tqk video {
                pointer-events: auto !important;
                position: relative;
                z-index: 9999 !important;
            }

            /* Disable other potential overlay elements */
            [role="presentation"],
            [data-pagelet],
            [data-visualcompletion] {
                pointer-events: none !important;
            }

            /* Make sure clickable elements remain clickable */
            [role="button"],
            [tabindex],
            button,
            a {
                pointer-events: auto !important;
            }
        `;
        document.head.appendChild(style);
    }

    // Enhanced unmute function
    function attemptUnmute(video) {
        if (!(video instanceof HTMLVideoElement)) return;

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

            if (video.audioTracks && video.audioTracks.length > 0) {
                for (let track of video.audioTracks) {
                    track.enabled = true;
                }
            }
            video.dispatchEvent(new Event('volumechange', { bubbles: true }));
        }
    }

    // Enable controls for Reels videos
    function enableReelControls(video) {
        if (!video || video.hasAttribute('controls') || !location.href.includes('reel')) return;

        // Add controls to the video
        video.setAttribute('controls', '');
        video.setAttribute('playsinline', '');

        // Style the video and handle overlays
        setTimeout(() => {
            try {
                // Style the video
                Object.assign(video.style, {
                    'position': 'relative',
                    'zIndex': '99999',
                    'pointerEvents': 'auto',
                    'width': '100%',
                    'height': 'auto',
                    'maxHeight': '100vh'
                });

                // Find and disable overlays
                const overlays = document.querySelectorAll(`
                    .x1ey2m1c.x78zum5.xdt5ytf.xozqiw3.x17qophe.x13a6bvl.x10l6tqk,
                    [role="presentation"],
                    [data-pagelet],
                    [data-visualcompletion]
                `);

                overlays.forEach(overlay => {
                    if (!overlay.contains(video)) {
                        overlay.style.pointerEvents = 'none';
                        overlay.style.opacity = '0.01';
                    }
                });

                // Ensure video is in view
                video.scrollIntoViewIfNeeded();
            } catch (e) {
                console.error('Error in enableReelControls:', e);
            }
        }, 100);
    }

    // Handle play events
    function handlePlayEvent(event) {
        try {
            const target = event.target;
            if (!(target instanceof HTMLVideoElement)) return;

            enableReelControls(target);
            attemptUnmute(target);

            // Additional check for muted state after a short delay
            setTimeout(() => attemptUnmute(target), 500);
        } catch (e) {
            console.error('Error in handlePlayEvent:', e);
        }
    }

    // Handle click events for mute/unmute buttons
    function handleClickEvent(event) {
        try {
            const target = event.target;
            const isMuteButton = target.closest(`
                [aria-label*="mute"],
                [aria-label*="sound"],
                [role="button"][aria-pressed],
                [aria-label*="sonido"],
                [aria-label*="silenciar"]
            `);

            if (isMuteButton) {
                setTimeout(() => {
                    document.querySelectorAll('video').forEach(attemptUnmute);
                }, 100);
            }
        } catch (e) {
            console.error('Error in handleClickEvent:', e);
        }
    }

    // Prevent pausing when switching tabs
    function handleVisibilityChange() {
        try {
            // Force visible state
            Object.defineProperty(document, 'visibilityState', {
                get: () => 'visible',
                configurable: true
            });

            Object.defineProperty(document, 'hidden', {
                get: () => false,
                configurable: true
            });

            // Resume any paused videos
            document.querySelectorAll('video').forEach(video => {
                if (video.paused && video.readyState >= 2) {
                    video.play().catch(e => console.log('Auto-play failed:', e));
                }
            });
        } catch (e) {
            console.error('Error in handleVisibilityChange:', e);
        }
    }

    // Override pause method
    function initPauseHandler() {
        const originalPause = HTMLMediaElement.prototype.pause;
        HTMLMediaElement.prototype.pause = function() {
            try {
                if (this.closest('[href*="reel"], [data-pagelet*="reel"], [class*="reel"]')) {
                    return; // Prevent reels from pausing
                }
                return originalPause.apply(this, arguments);
            } catch (e) {
                console.error('Error in pause handler:', e);
                return originalPause.apply(this, arguments);
            }
        };
        return originalPause;
    }

    // Initialize
    function init() {
        try {
            console.log('Facebook Reels Enhancer initialized');

            // Add styles first
            addStyles();

            // Set up pause handler
            const originalPause = initPauseHandler();

            // Set up event listeners
            document.addEventListener('play', handlePlayEvent, true);
            document.addEventListener('click', handleClickEvent, true);
            document.addEventListener('visibilitychange', handleVisibilityChange, true);

            // Set up MutationObserver for dynamically loaded content
            const observer = new MutationObserver(mutations => {
                for (const mutation of mutations) {
                    if (mutation.addedNodes) {
                        mutation.addedNodes.forEach(node => {
                            // Check if node is a video
                            if (node.nodeName === 'VIDEO') {
                                enableReelControls(node);
                                attemptUnmute(node);
                            }
                            // Check for video elements within added nodes
                            else if (node.querySelectorAll) {
                                const videos = node.querySelectorAll('video');
                                videos.forEach(video => {
                                    enableReelControls(video);
                                    attemptUnmute(video);
                                });
                            }
                        });
                    }
                }
            });

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

            // Initial check for videos
            document.querySelectorAll('video').forEach(video => {
                enableReelControls(video);
                attemptUnmute(video);
            });

            // Clean up on page unload
            window.addEventListener('beforeunload', () => {
                document.removeEventListener('play', handlePlayEvent, true);
                document.removeEventListener('click', handleClickEvent, true);
                document.removeEventListener('visibilitychange', handleVisibilityChange, true);
                observer.disconnect();
                HTMLMediaElement.prototype.pause = originalPause;
            });
        } catch (e) {
            console.error('Error initializing Facebook Reels Enhancer:', e);
        }
    }

    // Start the script
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        // If document is already loaded, wait a bit to ensure everything is ready
        setTimeout(init, 500);
    }
})();