UNA 14.0.0 Timeline Autoplay Sound Conflict: Multiple Videos Playing Together
In UNA 14.0.0, when Videos autoplay (for Timeline view only) is enabled with sound and Videos preload is set to auto in Card settings from Studio, there’s an issue on the Timeline.
When scrolling up and down with multiple videos one after another, the sound from several videos plays simultaneously.
The expected behavior is for the Timeline to focus on a single video autoplay and sound should trigger only for the video currently visible in the viewport, not for all of them at the same time.
As users scroll through the Timeline, only one video should autoplay with sound, while the others should remain paused and muted until they come into view.
-
-
·
LeonidS
- ·
Hello @Romulus !
A similar issue may occur when the video plays with focus on the video itself and extra space above and below the player. This can sometimes trigger multiple videos to play simultaneously. The 'Enabled without sound' option appears to be the most universal solution.
-
·
LeonidS
-
- · Romulus
-
·
In reply to LeonidS
- ·
Thank you for the suggestion, but unfortunately it doesn’t resolve the issue. I was specifically referring to the autoplay option as not functioning as expected. The core problem lies with the player itself, not the audio. It consumes more bandwidth than necessary, which not only affects performance but also significantly increases maintenance and operational costs. This represents wasted resources that quickly add up.
This isn’t an isolated issue affecting a single user. At scale, when managing tens of thousands or even millions of concurrent users, server maintenance costs increase dramatically.
Auto-play functionality should initiate a single streaming session per user. At present, the player creates unnecessary or multiple connections for the same user, leading to excessive bandwidth consumption. Beyond being inefficient, this results in avoidable strain on infrastructure and higher long-term operational expenses, particularly in high-traffic environments.
The idea of having an auto-play feature is excellent and certainly enhances the user experience. However, a feature that is still in beta or not fully functional has no place in a release officially labeled as stable.
Introducing incomplete or unreliable features in a stable release risks undermining the platform’s credibility and can cause significant operational issues at scale. It’s essential that such features are either properly finalized and thoroughly tested before release, or clearly marked as experimental and disabled by default.
Moreover, if multiple playback sessions aren’t initiated, there wouldn’t be multiple audio streams either. The issue is directly tied to redundant streaming sessions generated by the player itself. Resolving this would not only optimize bandwidth usage but also eliminate the overlapping audio issue entirely.
-
- · Brand Harbor
- ·
I concur with @Romulus this is a major issue that needs to be addressed. In our experience with the videos module, it has a long way to go to be a suitable video solution when compared with what "every" other social media platform is offering for a viewing and upload experience.
-
- · Romulus
- ·
Improvement Suggestion: Video Autoplay Single-Session Handling in Timeline
We’ve improved the video autoplay logic in the
BxTimelineView
class to ensure a better user experience. In the original implementation, multiple videos could start playing simultaneously when scrolled into view, causing audio overlap and performance issues.What we changed:
- Added playback control in
initVideosAutoplay
: Now, when a video starts playing via playerjs, it pauses all other initialized players before continuing. - Enhanced
autoplayVideos
logic: Instead of attempting to play/pause all videos on scroll, it now calculates which video is most visible in the viewport and ensures only that one plays, while pausing any others. - Tracked the currently playing video using
_sCurrentPlayerKey
to avoid redundant operations and maintain consistent state. - Kept backward compatibility with the original functions and admin options (
on
,on_mute
,off
).
This solution works well on the homepage timeline but in the the organization timeline however, I encounter an issue when transitioning from one video to another. It appears that for a brief moment, the videos still play simultaneously.
Any suggestion is welcome:
https://github.com/kabballa/timeline/commit/ee11eb89107b81c199ce9c9fcf8b5b42a463be5a
Why it matters:
This ensures a clean, intuitive browsing experience only one video plays at a time, without sound overlaps or excessive system resource use.
NOTE this is a test not a final solution
/** * 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 oParent.find('iframe[id]').each(function() { var frame = this; var key = sPrefix + '_' + frame.id; if (window.glBxTimelineVapPlayers[key]) return; var 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) { // ignore } } } }); window.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); }); } }); }; /** * Autoplay the single most visible video on scroll: pause the previous and play 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; } }); // If a new best: pause old, play newif (bestKey !== this._sCurrentPlayerKey) {if (this._sCurrentPlayerKey && window.glBxTimelineVapPlayers[this._sCurrentPlayerKey]) {try { window.glBxTimelineVapPlayers[this._sCurrentPlayerKey].pause(); } catch (e) { // ignore } } 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; };
-
- · Romulus
- ·
✅ 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:
- Navigate to the UNA Directory
- Use the terminal to move into your UNA installation directory:
cd una
- Remove the Existing File
- Delete the old JavaScript file:
rm modules/boonex/timeline/js/view.js
- Download and Apply the Patched File
- 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
- Set Correct File Permissions
- Set permissions to allow the owner to read and write, and others to read:
chmod 644 modules/boonex/timeline/js/view.js
- Permissions breakdown:
- Owner: read & write
- Group: read
- Others: read
- Set Correct File Ownership
- 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
- You can verify the current ownership of nearby files using:
ls -l modules/boonex/timeline/js/
- Verify the Changes
- Confirm permissions and ownership:
ls -l modules/boonex/timeline/js/view.js
- Example output:
-rw-r--r-- 1 www-data www-data 12345 May 7 23:00 modules/boonex/timeline/js/view.js
- Clear All Caches
- 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:
- Remove the Patched File
rm modules/boonex/timeline/js/view.js
- 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
- Reset Permissions and Ownership
chmod 644 modules/boonex/timeline/js/view.js chown www-data:www-data modules/boonex/timeline/js/view.js
- 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(); } }); };
-
👏👏👏
-
- · Brand Harbor
- ·
Beautiful @Romulus !
-
Thank you for your kind words; I hope you find it helpful.
-
I will let you know when we deploy but first we need to install some apache modules our new server seems to be missing.
-
You may need to restore permissions and verify the file owner. Please check all the instructions. I recommend using NGINX instead of Apache for better performance with UNA.
-
Will this also affect embedded videos?
-
Not yet, but I'm working on it. I also want to address the video viewing in albums and picture-in-picture (PiP) views by disabling autoplay on desktop for users with a mouse who want to use PiP. It would be great if that works. When I have time, I'll tackle that as well, but I'm not in a hurry since I have other priorities.
This was the worst error, as we eliminated excessive bandwidth usage, which can significantly impact a site with many hosted videos and double or triple maintenance costs. More details will follow.
But that's another issue. Why not open a new topic for embedded videos?
-
- · Romulus
-
·
In reply to Romulus
- ·
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 toplayCentralVideo
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!
-
- · Romulus
-
·
In reply to Romulus
- ·
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: