Comment to 'UNA 14.0.0 Timeline Autoplay Sound Conflict: Multiple Videos Playing Together'
  • ✅ Fixed: Timeline 14.0.10 - Video Autoplay with Sound in UNA 14.0.0

    Intersection Observer API : Resources and Patch Guide for UNA CMS Timeline 14.0.10

    Patch Installation Guide for UNA CMS Timeline 14.0.10

    Steps:

    1. Navigate to the UNA Directory
    2. Use the terminal to move into your UNA installation directory:
    cd una
    
    1. Remove the Existing File
    2. Delete the old JavaScript file:
    rm modules/boonex/timeline/js/view.js
    
    1. Download and Apply the Patched File
    2. Fetch the patched file from my repository and overwrite the old one:
    curl -o modules/boonex/timeline/js/view.js https://raw.githubusercontent.com/kabballa/timeline/14.0.10/js/view.js
    
    1. Set Correct File Permissions
    2. Set permissions to allow the owner to read and write, and others to read:
    chmod 644 modules/boonex/timeline/js/view.js
    
    1. Permissions breakdown:
    • Owner: read & write
    • Group: read
    • Others: read
    1. Set Correct File Ownership
    2. Ensure the file is owned by the correct web server user (replace www-data with your actual web user if different):
    chown www-data:www-data modules/boonex/timeline/js/view.js
    
    1. You can verify the current ownership of nearby files using:
    ls -l modules/boonex/timeline/js/
    
    1. Verify the Changes
    2. Confirm permissions and ownership:
    ls -l modules/boonex/timeline/js/view.js
    
    1. Example output:
    -rw-r--r-- 1 www-data www-data 12345 May  7 23:00 modules/boonex/timeline/js/view.js
    
    1. Clear All Caches
    2. Clear UNA’s system and JS caches to apply changes immediately.

    ⚠️ Note:

    This patch is a temporary solution for Timeline module v14.0.10 and only affects videos uploaded via the UNA Video module. Future UNA updates might overwrite this modification. I encourage the UNA team to review, refine, and integrate this improvement into upcoming releases.

    If you wish to revert to the original file:

    1. Remove the Patched File
    rm modules/boonex/timeline/js/view.js
    
    1. Restore the Original UNA Version
    curl -o modules/boonex/timeline/js/view.js https://raw.githubusercontent.com/unacms/una/refs/tags/14.0.0/modules/boonex/timeline/js/view.js
    
    1. Reset Permissions and Ownership
    chmod 644 modules/boonex/timeline/js/view.js
    chown www-data:www-data modules/boonex/timeline/js/view.js
    
    1. Clear All Caches Again

    🔧 Additional Patches

    For more patches on progress and improvements, visit my repo: https://github.com/kabballa/timeline/tree/14.0.10

    # -------------------------------------------------------------------------------------- #
    #                                                                                        #
    #   ⚠️  IMPORTANT NOTICE                                                                 #
    #                                                                                        #
    #       These commands and configuration changes are for informational purposes only.    #
    #       They DO NOT represent an official setup guide.                                   #
    #                                                                                        #
    #   ⚙️  Run them ONLY if you fully understand what they do.                              #
    #                                                                                        #
    #   ❗ Use at your own risk — this code can break your website.                          #
    #                                                                                        #
    #      This example is intended as a proposal for development code and should only       #
    #      be used on a test server, not in production.                                      #
    #                                                                                        #
    #      Ensure the configuration values are appropriate for your environment and adjust   #
    #      as needed.                                                                        #
    #                                                                                        #
    #   👉 This example is intended for advanced users only.                                 #
    #                                                                                        #
    # -------------------------------------------------------------------------------------- #
    

    Here you can see my proposal, however, be cautious not to copy from here, as the editor may alter the code formatting. It’s best to copy the code directly from GitHub. :

    /**
     * Initialize iframe players and enforce single-session playback: only one video plays at a time.
     * Ensures that before any video plays, all others are paused.
     */
    BxTimelineView.prototype.initVideosAutoplay = function(oParent) {
        var $this = this;
        if (this._sVideosAutoplay === 'off') return;
    
        // Preserve original initializationthis.initVideos(oParent);
    
        var sPrefix = oParent.hasClass(this.sClassView)
            ? oParent.attr('id')
            : oParent.parents('.' + this.sClassView + ':first').attr('id');
    
        // Register each iframe player once, without deleting existing references
        oParent.find('iframe[id]').each(function() {
            var frame = this;
            var key = sPrefix + '_' + frame.id;
    
            // If the player already exists, skip reinitializationif (window.glBxTimelineVapPlayers[key]) {console.log(`Player ${key} already exists. Skipping reinitialization.`);return;
            }
    
            // Initialize the new playervar player = new playerjs.Player(frame);if ($this._sVideosAutoplay === 'on_mute') player.mute();
    
            // Sync container height on ready/playvar fFixHeight = function() {var video = $('#' + key).contents().find('video');if (video.length) $('#' + key).height(video.height() + 'px');
            };
            player.on('ready', fFixHeight);
            player.on('play', fFixHeight);
    
            // When this player starts, pause all others
            player.on('play', function() {
                for (var k in window.glBxTimelineVapPlayers) {
                    if (k !== key) {
                        try {
                            window.glBxTimelineVapPlayers[k].pause();
                        } catch (e) {
                            console.warn(`Unable to pause player ${k}`, e);
                        }
                    }
                }
            });
    
            // Save the new playerwindow.glBxTimelineVapPlayers[key] = player;
        });
    
        // Reset current session trackingthis._sCurrentPlayerKey = null;
    
        // Bind original scroll autoplay
        $(window).off('scroll.timelineVap').on('scroll.timelineVap', function() {
            if (!$this.oView.is(':visible')) return;
            if (!window.requestAnimationFrame) {
                setTimeout(function() {
                    $this.autoplayVideos($this.oView, $this._fVapOffsetStart, $this._fVapOffsetStop);
                }, 100);
            } else {
                window.requestAnimationFrame(function() {
                    $this.autoplayVideos($this.oView, $this._fVapOffsetStart, $this._fVapOffsetStop);
                });
            }
        });
    
        // Trigger autoplay immediately on page loadwindow.requestAnimationFrame(function() {
            $this.autoplayVideos($this.oView, $this._fVapOffsetStart, $this._fVapOffsetStop);
        });
    };
    
    /**
     * Autoplay the single most visible video on scroll: pause all others before playing the new one.
     */
    BxTimelineView.prototype.autoplayVideos = function(oView, fOffsetStart, fOffsetStop) {
        var sPrefix = oView.attr('id') + '_';
        var winTop = $(window).scrollTop();
        var winH = $(window).height();
        var bestKey = null;
        var bestRatio = 0;
    
        oView.find('.' + this.sClassItem + ' iframe[id]').each(function() {
            var $f = $(this);
            var top = $f.offset().top;
            var bottom = top + $f.height();
            var visTop = Math.max(top, winTop);
            var visBottom = Math.min(bottom, winTop + winH);
            var ratio = Math.max(0, visBottom - visTop) / $f.height();
            var key = sPrefix + this.id;
            if (ratio > bestRatio) {
                bestRatio = ratio;
                bestKey = key;
            }
        });
    
        // Pause all players before playing the new onefor (var k in window.glBxTimelineVapPlayers) {if (k !== bestKey) {try {window.glBxTimelineVapPlayers[k].pause();
                } catch (e) {
                    // ignore
                }
            }
        }
    
        // Play the best visible video if it's different from the currentif (bestKey !== this._sCurrentPlayerKey) {if (bestKey && window.glBxTimelineVapPlayers[bestKey]) {try {window.glBxTimelineVapPlayers[bestKey].play();
                } catch (e) {
                    // ignore
                }
            }
            this._sCurrentPlayerKey = bestKey;
        }
    };
    
    /**
     * Manually trigger autoplay logic.
     */
    BxTimelineView.prototype.playVideos = function(oView) {
        if (this._sVideosAutoplay === 'off')
            return;
        this.autoplayVideos(oView, this._fVapOffsetStart, this._fVapOffsetStop);
    };
    
    /**
     * Immediately pause all videos and reset the current key.
     */
    BxTimelineView.prototype.pauseVideos = function(oView) {
        for (var k in window.glBxTimelineVapPlayers) {
            try {
                window.glBxTimelineVapPlayers[k].pause();
            } catch (e) {
                // ignore
            }
        }
        this._sCurrentPlayerKey = null;
    };
    
    BxTimelineView.prototype._initCentralVideoObserver = function(aVideoElements) {
        var $this = this;
        var oCurrentlyPlaying = null;
    
        var observer = new IntersectionObserver(function(entries) {
            entries.forEach(function(entry) {
                var oVideoElement = entry.target;
                var sVideoId = oVideoElement.getAttribute('data-bx-timeline-video-id');
                var oVideoData = aVideoElements.find(function(v) {
                    return v.id === sVideoId;
                });
    
                if (oVideoData) {
                    if (entry.isIntersecting) {
                        if (oCurrentlyPlaying && oCurrentlyPlaying.id!== oVideoData.id) {
                            oCurrentlyPlaying.player.pause();
                        }
                        oVideoData.player.play();
                        oCurrentlyPlaying = oVideoData;
                        // Sound autoplay is often restricted by browsers.// Consider if you need to unmute based on user interaction.if ($this._sVideosAutoplay == 'on') {
                            oVideoData.player.unmute();
                        } else if ($this._sVideosAutoplay == 'on_mute') {
                            oVideoData.player.mute();
                        }
                    } else {
                        if (oCurrentlyPlaying && oCurrentlyPlaying.id === sVideoId) {
                            oCurrentlyPlaying.player.pause();
                            oCurrentlyPlaying = null;
                        }
                    }
                }
            });
        }, {
            rootMargin: '-50% 0px -50% 0px', // Observe when the top or bottom edge enters the centerthreshold: 0
        });
    
        aVideoElements.forEach(function(oVideo) {
            observer.observe(oVideo.element);
        });
    };
    
    /**
     * Fallback scroll-based autoplay: preserves original behavior.
     */
    BxTimelineView.prototype.autoplayVideos = function(oView, fOffsetStart, fOffsetStop) {
        var $this = this;
    
        var oItems = oView.find('.' + this.sClassItem);
        var sPrefix = oView.attr('id') + '_';
    
        oItems.each(function() {
            $(this).find('iframe').each(function() {
                var oFrame = $(this);
                var sPlayerId = sPrefix + oFrame.attr('id');
                var oPlayer = window.glBxTimelineVapPlayers[sPlayerId];
                if (!oPlayer) {
                    return;
                }
    
                var iFrameTop = oFrame.offset().top;
                var iFrameBottom = iFrameTop + oFrame.height();
                var iWindowTop = $(window).scrollTop();
                var iWindowHeight = $(window).height();
                if (iFrameTop <= iWindowTop + iWindowHeight * fOffsetStart && iFrameBottom >= iWindowTop + iWindowHeight * fOffsetStop) {
                    oPlayer.play();
                } else {
                    oPlayer.pause();
                }
            });
        });
    };
    
    /**
     * Manually trigger central-selection playback.
     */
    BxTimelineView.prototype.playVideos = function(oView) {
        var $this = this;
        var sPrefix = oView.attr('id') + '_';
        var oCentralVideo = this._getCentralVideoInView(oView);
    
        oView.find('iframe').each(function() {
            var $iframe = $(this);
            var sPlayerId = sPrefix + $iframe.attr('id');
            var oPlayer = window.glBxTimelineVapPlayers[sPlayerId];
            if (oPlayer) {
                oPlayer.pause();
            }
        });
    
        if (oCentralVideo && oCentralVideo.player) {
            oCentralVideo.player.play();
            if ($this._sVideosAutoplay == 'on') {
                oCentralVideo.player.unmute();
            } else if ($this._sVideosAutoplay == 'on_mute') {
                oCentralVideo.player.mute();
            }
        }
    };
    
    BxTimelineView.prototype._getCentralVideoInView = function(oView) {
        var $this = this;
        var oCentralVideo = null;
        var iViewportHeight = $(window).height();
        var iCenterViewport = iViewportHeight / 2;
        var iMinOffset = Infinity;
        var sPrefix = oView.attr('id') + '_';
    
        oView.find('iframe').each(function() {
            var $iframe = $(this);
            var sPlayerId = sPrefix + $iframe.attr('id');
            var oPlayer = window.glBxTimelineVapPlayers[sPlayerId];
            if (!oPlayer) {
                return;
            }
    
            var iVideoTop = $iframe.offset().top;
            var iVideoHeight = $iframe.height();
            var iVideoCenter = iVideoTop + (iVideoHeight / 2);
            var iOffset = Math.abs(iVideoCenter - iCenterViewport);
    
            if (iOffset < iMinOffset) {
                iMinOffset = iOffset;
                oCentralVideo = {
                    id: sPlayerId,
                    player: oPlayer
                };
            }
        });
        return oCentralVideo;
    };
    
    /**
     * Immediately pause & mute all videos.
     */
    BxTimelineView.prototype.pauseVideos = function(oView) {
        var $this = this;
        var sPrefix = oView.attr('id') + '_';
    
        oView.find('iframe').each(function() {
            var $iframe = $(this);
            var sPlayerId = sPrefix + $iframe.attr('id');
            var oPlayer = window.glBxTimelineVapPlayers[sPlayerId];
            if (oPlayer) {
                oPlayer.pause();
                oPlayer.mute();
            }
        });
    };
    
    • 👏👏👏

      • Updates for 2025-05-09

        • Added a robust autoplay system for video players in the Timeline module.
        • Uses IntersectionObserver to detect when a video becomes visible in the viewport and triggers autoplay.
        • Implemented a fallback mechanism for browsers without IntersectionObserver support, using scroll-based detection.
        • Refactored player registration to enforce single-session playback only one video plays at a time.
        • Improved the autoplayVideos logic:
        • Prioritizes playing the most visible video in the viewport.
        • Automatically pauses all other videos.
        • Added the autoplayVideosFallback function:
        • In the absence of IntersectionObserver, it plays the video closest to the center of the viewport.
        • Standardized and updated code comments for:
        • initVideosAutoplay
        • autoplayVideos
        • autoplayVideosFallback
        • Improving code readability and maintainability.
        • Consolidated all video pause functionalities into a single pauseVideos method:
        • Supports pausing all videos globally or only those within a specific view.
        • Refactored the playVideos function to playCentralVideo for better clarity:
        • The new name accurately reflects its role in managing playback for the central (most visible) video.
        • Added debug log messages for function calls (to be cleaned up later).
        • Fully tested all functionality across:
        • Browsers with IntersectionObserver support.
        • Browsers without IntersectionObserver support.
        • Confirmed single-session playback enforcement in all cases.
        • No regressions found in other related modules:
        • Outline
        • Infinite Scroll
        • See More

        📌 Full list of commits:

        https://github.com/kabballa/timeline/commits/14.0.10/

        📄 For full update details and README:

        https://github.com/kabballa/timeline/tree/14.0.10

        These updates ensure a consistent, reliable, and seamless autoplay experience for videos in the Timeline, regardless of browser capabilities.

        Happy coding!

        • Latest Update from today:

          • Added comprehensive and consistent JSDoc comments to all functions in the file to improve code readability, maintainability, and facilitate developer onboarding.
          • Implemented a debounce utility function and applied it to the infinite scroll handler in the timeline view to optimize scroll performance and minimize UI blocking during dynamic content loading.
          • Enhanced video autoplay logic by:
          • Adding play/pause state checks before calling video methods.
          • Suppressing AbortError warnings from autoplay promises to prevent console clutter during rapid timeline scrolling.

          Benefits:

          Improves application responsiveness during heavy scrolling, reduces unnecessary function calls, prevents autoplay errors, and makes the codebase easier to maintain and extend.

           For full update details and README: