From 101f2e786a220847f77a256c768344a58512a843 Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Thu, 21 May 2026 13:42:42 -0700 Subject: [PATCH] feat(PE-1271): this change introduces early start on the preloaded content to avoid black screens during transitions --- examples/seamless-video-switching/index.html | 2 +- examples/seamless-video-switching/index.js | 118 +++++++++---------- 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/examples/seamless-video-switching/index.html b/examples/seamless-video-switching/index.html index 2a3f751..0fcfdde 100644 --- a/examples/seamless-video-switching/index.html +++ b/examples/seamless-video-switching/index.html @@ -52,7 +52,7 @@
-
+ diff --git a/examples/seamless-video-switching/index.js b/examples/seamless-video-switching/index.js index 53a6cc6..d246f72 100644 --- a/examples/seamless-video-switching/index.js +++ b/examples/seamless-video-switching/index.js @@ -9,52 +9,93 @@ let currentVideoIndex = 0; let visibleVideoPlayer = 1; let videoFiles = []; -// Helper function to get player element by number function getPlayer(playerNumber) { return document.getElementById(`video-player-${playerNumber}`); } -// Helper function to get the currently visible and hidden players function getPlayers() { const visible = getPlayer(visibleVideoPlayer); const hidden = getPlayer(visibleVideoPlayer === 1 ? 2 : 1); return { visible, hidden }; } +function preloadNextVideo() { + const nextVideoIndex = (currentVideoIndex + 1) % videoFiles.length; + const { hidden } = getPlayers(); + hidden.pause(); + hidden.currentTime = 0; + hidden.muted = true; + hidden.src = `${assetsFolder}/${videoFiles[nextVideoIndex]}`; + hidden.load(); + + hidden.addEventListener('canplay', () => { + hidden.play().then(() => { hidden.pause(); hidden.currentTime = 0; }).catch(() => {}); + }, { once: true }); +} + +// Start hidden player 0.5s before end so it's already playing when the swap happens +function armEarlyStart() { + const { visible } = getPlayers(); + visible.addEventListener('timeupdate', function onNearEnd() { + if (visible.duration - visible.currentTime <= 0.5) { + visible.removeEventListener('timeupdate', onNearEnd); + getPlayers().hidden.play().catch(() => {}); + } + }); +} + +function switchToNextVideo() { + currentVideoIndex = (currentVideoIndex + 1) % videoFiles.length; + const { visible: currentPlayer, hidden: nextPlayer } = getPlayers(); + + const doSwap = () => { + currentPlayer.pause(); + currentPlayer.muted = true; + currentPlayer.classList.add('hidden'); + nextPlayer.muted = false; + nextPlayer.classList.remove('hidden'); + visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; + preloadNextVideo(); + armEarlyStart(); + }; + + // If early start worked, swap is instant; otherwise wait for playing + if (!nextPlayer.paused) { + doSwap(); + } else { + nextPlayer.play().catch(e => console.error('Play error:', e)); + nextPlayer.addEventListener('playing', doSwap, { once: true }); + } +} + async function main() { console.log('main() - App ready!'); try { - // Load and filter video files videoFiles = fs.readdirSync(`${rootStoragePath}/${assetsFolder}`); videoFiles = videoFiles.filter(filename => !filename.startsWith('.')); videoFiles.sort(); - - // console.log('Video files:', videoFiles); - + if (videoFiles.length === 0) { console.error(`No video files found in ${rootStoragePath}/${assetsFolder}`); return; } - + const player1 = getPlayer(1); const player2 = getPlayer(2); - - // Initialize first video + player1.src = `${assetsFolder}/${videoFiles[0]}`; player1.currentTime = 0; - player1.muted = false; // Ensure audio is enabled for first player - player2.muted = true; // Mute the second player initially - console.log('Player 1 loaded:', videoFiles[0]); - - // Set up video ended listeners for both players + player1.muted = false; + player2.muted = true; + player1.addEventListener('ended', () => switchToNextVideo()); player2.addEventListener('ended', () => switchToNextVideo()); - - // Preload next video once first video starts playing + player1.addEventListener('playing', () => { console.log('Player 1 playing'); preloadNextVideo(); + armEarlyStart(); }, { once: true }); } catch (e) { @@ -62,51 +103,6 @@ async function main() { } } -// Preload the next video in the hidden player -function preloadNextVideo() { - const nextVideoIndex = (currentVideoIndex + 1) % videoFiles.length; - const { hidden } = getPlayers(); - const filePath = `${assetsFolder}/${videoFiles[nextVideoIndex]}`; - - console.log(`Preloading: ${videoFiles[nextVideoIndex]}`); - - // Reset and load the next video - hidden.pause(); - hidden.currentTime = 0; - hidden.muted = true; // Ensure hidden player is muted - hidden.src = filePath; - hidden.load(); -} - -// Switch to the next video seamlessly -function switchToNextVideo() { - currentVideoIndex = (currentVideoIndex + 1) % videoFiles.length; - const { visible: currentPlayer, hidden: nextPlayer } = getPlayers(); - - console.log(`Switching to: ${videoFiles[currentVideoIndex]}`); - - // Ensure next player starts from beginning - nextPlayer.currentTime = 0; - - // Start playing the next video (while still hidden) - nextPlayer.play().catch(e => console.error('Play error:', e)); - - // Switch visibility and audio immediately - the video is already preloaded - currentPlayer.pause(); - currentPlayer.muted = true; // Mute the outgoing player - currentPlayer.classList.add('hidden'); - - nextPlayer.muted = false; // Unmute the incoming player - nextPlayer.classList.remove('hidden'); - - // Toggle which player is visible - visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; - - // Preload the next video in the now-hidden player - preloadNextVideo(); -} - -// Call main when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else {