From de9c5dd74817a85682adff228630e2e7d27c0837 Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Thu, 11 Jun 2026 09:04:02 -0700 Subject: [PATCH 1/7] feat(PE-1271): add seamless video switching example Dual HTML5 video element implementation for gap-free playback on BrightSign players. Background-preloads the next video so transitions are instant with no black frames or freeze. Includes review fixes from the original draft: - Add missing + + diff --git a/examples/browser/seamless-video-switching/index.js b/examples/browser/seamless-video-switching/index.js new file mode 100644 index 0000000..ff4b0b6 --- /dev/null +++ b/examples/browser/seamless-video-switching/index.js @@ -0,0 +1,112 @@ +const fs = require('fs'); + +// Configuration - edit these values as needed +const rootStoragePath = '/storage/sd'; +const assetsFolder = 'assets'; + +// State tracking +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 }; +} + +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(); + + 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.addEventListener('ended', () => switchToNextVideo()); + player2.addEventListener('ended', () => switchToNextVideo()); + + // Preload next video once first video starts playing + player1.addEventListener('playing', () => { + console.log('Player 1 playing'); + preloadNextVideo(); + }, { once: true }); + + } catch (e) { + console.error('Error in main():', e); + } +} + +// 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 { + main(); +} From 33e91d72b5d4408c743f8044eba2c4b4782cb5a3 Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Tue, 16 Jun 2026 16:13:06 -0400 Subject: [PATCH 2/7] fix(PE-1271): defer visibility swap until nextPlayer is playing Also note the Node fs dependency in the README listing. --- examples/README.md | 2 +- .../browser/seamless-video-switching/index.js | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/README.md b/examples/README.md index bb9ad09..b1528eb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -36,7 +36,7 @@ Note that some starter examples include the creation of a `brightsign-dumps` fol | [send-plugin-message](browser/send-plugin-message) | Plugin message communication between BrightScript and HTML/JS | 8.x, 9.x | | [syncmanager-js](browser/syncmanager-js) | Multi-player content synchronization using SyncManager JS API | 8.x, 9.x | | [large-file-download](browser/large-file-download) | Memory-bounded multi-GB download using Node streams in roHtmlWidget | 8.x, 9.x | -| [seamless-video-switching](browser/seamless-video-switching) | Gap-free HTML5 video playback using dual video elements with background preloading | 8.x, 9.x | +| [seamless-video-switching](browser/seamless-video-switching) | Gap-free HTML5 video playback using dual video elements with background preloading; reads asset list via Node `fs` in roHtmlWidget | 8.x, 9.x | ### Node.js 14 Examples (`node-14/`) diff --git a/examples/browser/seamless-video-switching/index.js b/examples/browser/seamless-video-switching/index.js index ff4b0b6..75bfff9 100644 --- a/examples/browser/seamless-video-switching/index.js +++ b/examples/browser/seamless-video-switching/index.js @@ -86,22 +86,25 @@ function switchToNextVideo() { // 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)); + // Defer the visibility swap until the hidden player is actually playing + // so we never reveal an unrendered frame. + nextPlayer.addEventListener('playing', () => { + currentPlayer.pause(); + currentPlayer.muted = true; // Mute the outgoing player + currentPlayer.classList.add('hidden'); - // 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'); - nextPlayer.muted = false; // Unmute the incoming player - nextPlayer.classList.remove('hidden'); + // Toggle which player is visible + visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; - // Toggle which player is visible - visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; + // Preload the next video in the now-hidden player + preloadNextVideo(); + }, { once: true }); - // Preload the next video in the now-hidden player - preloadNextVideo(); + // Start playing the next video (still hidden) + nextPlayer.play().catch(e => console.error('Play error:', e)); } // Call main when DOM is ready From f4d20395587eed1fe3b7bd0ef018fda591f0748c Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Tue, 16 Jun 2026 16:58:46 -0400 Subject: [PATCH 3/7] docs(PE-1271): added notes to supported content types and varied performance based on hardware and software versions --- examples/browser/seamless-video-switching/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/browser/seamless-video-switching/README.md b/examples/browser/seamless-video-switching/README.md index dd085bf..6078229 100644 --- a/examples/browser/seamless-video-switching/README.md +++ b/examples/browser/seamless-video-switching/README.md @@ -65,3 +65,8 @@ The application will automatically: - Sort them alphabetically - Play them in sequence with seamless transitions - Loop back to the first video after the last one finishes + +## Notes + +- **Supported video formats**: See [BrightSign video formats and codecs](https://docs.brightsign.biz/advanced/video-formats-and-codecs) for the list of containers, codecs, and profiles supported by each player series. +- **Performance**: Playback smoothness and switching behavior may vary depending on player model, OS version, video mode (resolution/framerate), and the encoding of your source files. Adjust the code (e.g., preload behavior, number of preloaded players) to fit your specific use-case. From 9de1524ce81a2a3de2e5b117224669b6eb26cc23 Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Fri, 19 Jun 2026 15:05:23 -0400 Subject: [PATCH 4/7] fix(PE-1271): improve seamless video switching reliability and add multi-decoder variant - Fix missing + + diff --git a/examples/browser/seamless-video-switching-multi-decoder/index.js b/examples/browser/seamless-video-switching-multi-decoder/index.js new file mode 100644 index 0000000..ec58cdf --- /dev/null +++ b/examples/browser/seamless-video-switching-multi-decoder/index.js @@ -0,0 +1,111 @@ +const fs = require('fs'); + +// Configuration - edit these values as needed +const rootStoragePath = '/storage/sd'; +const assetsFolder = 'assets'; + +// State tracking +let currentVideoIndex = 0; +let visibleVideoPlayer = 1; +let videoFiles = []; + +function getPlayer(playerNumber) { + return document.getElementById(`video-player-${playerNumber}`); +} + +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(); + + // Warm the decode pipeline so play() starts near-instantly at switch time + 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 { + videoFiles = fs.readdirSync(`${rootStoragePath}/${assetsFolder}`); + videoFiles = videoFiles.filter(filename => !filename.startsWith('.')); + videoFiles.sort(); + + if (videoFiles.length === 0) { + console.error(`No video files found in ${rootStoragePath}/${assetsFolder}`); + return; + } + + const player1 = getPlayer(1); + const player2 = getPlayer(2); + + player1.src = `${assetsFolder}/${videoFiles[0]}`; + player1.currentTime = 0; + player1.muted = false; + player2.muted = true; + + player1.addEventListener('ended', () => switchToNextVideo()); + player2.addEventListener('ended', () => switchToNextVideo()); + + player1.addEventListener('playing', () => { + console.log('Player 1 playing'); + preloadNextVideo(); + armEarlyStart(); + }, { once: true }); + + } catch (e) { + console.error('Error in main():', e); + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', main); +} else { + main(); +} diff --git a/examples/browser/seamless-video-switching/README.md b/examples/browser/seamless-video-switching/README.md index 6078229..a88bcd4 100644 --- a/examples/browser/seamless-video-switching/README.md +++ b/examples/browser/seamless-video-switching/README.md @@ -70,3 +70,4 @@ The application will automatically: - **Supported video formats**: See [BrightSign video formats and codecs](https://docs.brightsign.biz/advanced/video-formats-and-codecs) for the list of containers, codecs, and profiles supported by each player series. - **Performance**: Playback smoothness and switching behavior may vary depending on player model, OS version, video mode (resolution/framerate), and the encoding of your source files. Adjust the code (e.g., preload behavior, number of preloaded players) to fit your specific use-case. +- **Multi-decoder hardware**: On mid-to-high-end players (XT, 4K series) that support simultaneous decode pipelines, see [seamless-video-switching-multi-decoder](../seamless-video-switching-multi-decoder/) for near-zero-gap transitions using decoder pre-warming and early start. diff --git a/examples/browser/seamless-video-switching/index.js b/examples/browser/seamless-video-switching/index.js index 75bfff9..27617a3 100644 --- a/examples/browser/seamless-video-switching/index.js +++ b/examples/browser/seamless-video-switching/index.js @@ -9,23 +9,49 @@ 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(); +} + +function switchToNextVideo() { + currentVideoIndex = (currentVideoIndex + 1) % videoFiles.length; + const { visible: currentPlayer, hidden: nextPlayer } = getPlayers(); + + // Defer visibility swap until the hidden player is actually playing + // to avoid revealing an unrendered frame + nextPlayer.addEventListener('playing', () => { + currentPlayer.pause(); + currentPlayer.muted = true; + currentPlayer.classList.add('hidden'); + nextPlayer.muted = false; + nextPlayer.classList.remove('hidden'); + visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; + preloadNextVideo(); + }, { once: true }); + + nextPlayer.play().catch(e => console.error('Play error:', e)); +} + 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(); @@ -38,18 +64,14 @@ async function main() { 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]); + player1.muted = false; + player2.muted = true; - // Set up video ended listeners for both players 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(); @@ -60,54 +82,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; - - // Defer the visibility swap until the hidden player is actually playing - // so we never reveal an unrendered frame. - nextPlayer.addEventListener('playing', () => { - 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(); - }, { once: true }); - - // Start playing the next video (still hidden) - nextPlayer.play().catch(e => console.error('Play error:', e)); -} - -// Call main when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { From 64ea2d9c1be9401fabd43618c2701d3f107d3421 Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Fri, 19 Jun 2026 16:23:43 -0400 Subject: [PATCH 5/7] docs(PE-1271): improve single-decoder README cross-reference to multi-decoder variant --- examples/browser/seamless-video-switching/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/browser/seamless-video-switching/README.md b/examples/browser/seamless-video-switching/README.md index a88bcd4..6dd1252 100644 --- a/examples/browser/seamless-video-switching/README.md +++ b/examples/browser/seamless-video-switching/README.md @@ -18,6 +18,10 @@ 4. Video 1 (now hidden) loads the next file in the background 5. The cycle repeats for seamless continuous playback +## Which example should I use? + +This example works on any BrightSign player and is the recommended starting point. If your hardware supports multiple simultaneous decode pipelines, see [seamless-video-switching-multi-decoder](../seamless-video-switching-multi-decoder/) for near-zero-gap transitions using decoder pre-warming and early start. Refer to the [BrightSign model and series reference](https://docs.brightsign.biz/hardware/model-and-series-reference) to check your player's capabilities. + ## Configuration You can customize the following settings in `index.js` before deploying: From d21cea91abbc030f63f0a15d4c38c22535fafa4c Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Tue, 23 Jun 2026 12:23:52 -0400 Subject: [PATCH 6/7] refactor(PE-1271): consolidate multi-decoder variant into MULTI_DECODER flag --- .../README.md | 53 --------- .../autorun.brs | 38 ------ .../index.html | 51 -------- .../index.js | 111 ------------------ .../seamless-video-switching/README.md | 25 +++- .../seamless-video-switching/autorun.brs | 2 + .../browser/seamless-video-switching/index.js | 71 +++++++++-- 7 files changed, 82 insertions(+), 269 deletions(-) delete mode 100644 examples/browser/seamless-video-switching-multi-decoder/README.md delete mode 100644 examples/browser/seamless-video-switching-multi-decoder/autorun.brs delete mode 100644 examples/browser/seamless-video-switching-multi-decoder/index.html delete mode 100644 examples/browser/seamless-video-switching-multi-decoder/index.js diff --git a/examples/browser/seamless-video-switching-multi-decoder/README.md b/examples/browser/seamless-video-switching-multi-decoder/README.md deleted file mode 100644 index 7dfe1c7..0000000 --- a/examples/browser/seamless-video-switching-multi-decoder/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# BrightSign Dual Video Player — Seamless Playback (Multi-Decoder) - -> An enhanced seamless video player for BrightSign players with multiple hardware decode pipelines. Provides near-zero-gap transitions using decoder pre-warming and early start. - -## Hardware Requirements - -This example requires a BrightSign player capable of running two hardware decode pipelines simultaneously — suited for mid-to-high-end series (XT, 4K). On single-decoder hardware, the pre-warming and early start will compete with the active player's decoder and may cause stuttering. Use the [seamless-video-switching](../seamless-video-switching/) example instead. - -## How It Works - -Builds on the same dual video element technique as the base example, with two additions: - -1. **Decoder pre-warming** — after the hidden player buffers (`canplay`), it plays briefly then pauses at frame 0. This initializes the hardware decode pipeline so `play()` at switch time starts near-instantly. -2. **Early start** — the hidden player begins playing (muted, hidden) 0.5s before the visible video ends. By the time `ended` fires, the hidden player is already producing frames and the swap is immediate with no gap. - -If the early start did not complete in time, the switch falls back to awaiting the `playing` event before swapping visibility, preventing a swap onto an unrendered frame. - -## Trade-offs vs. Base Example - -| | This example | seamless-video-switching | -|---|---|---| -| Hardware | Multi-decoder required | Any | -| Gap at transition | Near-zero | Small gap (decoder handoff) | -| Simultaneous decoding | Yes | No | - -## Configuration - -- **`rootStoragePath`** — storage path on the player (default: `/storage/sd`) -- **`assetsFolder`** — folder containing your video files (default: `assets`) - -## SD Card Structure - -``` -SD/ -├── autorun.brs -├── index.html -├── index.js -└── assets/ - ├── video1.mp4 - └── video2.mp4 -``` - -## Deployment Steps - -1. Copy `autorun.brs`, `index.html`, and `index.js` to the root of your SD card -2. Create an `assets/` folder and copy your video files into it -3. Insert the SD card and power on the player - -## Notes - -- **Supported video formats**: See [BrightSign video formats and codecs](https://docs.brightsign.biz/advanced/video-formats-and-codecs) for supported containers, codecs, and profiles by player series. -- **Early start threshold**: The 0.5s window in `armEarlyStart()` can be adjusted to trade content budget against transition reliability on your specific hardware and content type. -- **Performance**: Switching smoothness may vary by player model, OS version, video resolution, and source encoding. diff --git a/examples/browser/seamless-video-switching-multi-decoder/autorun.brs b/examples/browser/seamless-video-switching-multi-decoder/autorun.brs deleted file mode 100644 index aa6e8a8..0000000 --- a/examples/browser/seamless-video-switching-multi-decoder/autorun.brs +++ /dev/null @@ -1,38 +0,0 @@ -function main() - mp = CreateObject("roMessagePort") - - ' Create HTML Widget - widget = CreateHTMLWidget(mp) - widget.Show() - - 'Event Loop - while true - msg = wait(0, mp) - print "msg received - type=";type(msg) - if type(msg) = "roHtmlWidgetEvent" then - print "msg: ";msg - end if - end while - -end function - -function CreateHTMLWidget(mp as object) as object - ' Get Screen Resolution - vidmode = CreateObject("roVideoMode") - width = vidmode.GetResX() - height = vidmode.GetResY() - - r = CreateObject("roRectangle", 0, 0, width, height) - - ' Create HTML Widget config - config = { - nodejs_enabled: true, - url: "file:///sd:/index.html", - port: mp - } - - ' Create HTML Widget - h = CreateObject("roHtmlWidget", r, config) - return h - -end function diff --git a/examples/browser/seamless-video-switching-multi-decoder/index.html b/examples/browser/seamless-video-switching-multi-decoder/index.html deleted file mode 100644 index d91b86a..0000000 --- a/examples/browser/seamless-video-switching-multi-decoder/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - Seamless Video App - - - -
- - -
- - - diff --git a/examples/browser/seamless-video-switching-multi-decoder/index.js b/examples/browser/seamless-video-switching-multi-decoder/index.js deleted file mode 100644 index ec58cdf..0000000 --- a/examples/browser/seamless-video-switching-multi-decoder/index.js +++ /dev/null @@ -1,111 +0,0 @@ -const fs = require('fs'); - -// Configuration - edit these values as needed -const rootStoragePath = '/storage/sd'; -const assetsFolder = 'assets'; - -// State tracking -let currentVideoIndex = 0; -let visibleVideoPlayer = 1; -let videoFiles = []; - -function getPlayer(playerNumber) { - return document.getElementById(`video-player-${playerNumber}`); -} - -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(); - - // Warm the decode pipeline so play() starts near-instantly at switch time - 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 { - videoFiles = fs.readdirSync(`${rootStoragePath}/${assetsFolder}`); - videoFiles = videoFiles.filter(filename => !filename.startsWith('.')); - videoFiles.sort(); - - if (videoFiles.length === 0) { - console.error(`No video files found in ${rootStoragePath}/${assetsFolder}`); - return; - } - - const player1 = getPlayer(1); - const player2 = getPlayer(2); - - player1.src = `${assetsFolder}/${videoFiles[0]}`; - player1.currentTime = 0; - player1.muted = false; - player2.muted = true; - - player1.addEventListener('ended', () => switchToNextVideo()); - player2.addEventListener('ended', () => switchToNextVideo()); - - player1.addEventListener('playing', () => { - console.log('Player 1 playing'); - preloadNextVideo(); - armEarlyStart(); - }, { once: true }); - - } catch (e) { - console.error('Error in main():', e); - } -} - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', main); -} else { - main(); -} diff --git a/examples/browser/seamless-video-switching/README.md b/examples/browser/seamless-video-switching/README.md index 6dd1252..33c68c2 100644 --- a/examples/browser/seamless-video-switching/README.md +++ b/examples/browser/seamless-video-switching/README.md @@ -18,25 +18,40 @@ 4. Video 1 (now hidden) loads the next file in the background 5. The cycle repeats for seamless continuous playback -## Which example should I use? - -This example works on any BrightSign player and is the recommended starting point. If your hardware supports multiple simultaneous decode pipelines, see [seamless-video-switching-multi-decoder](../seamless-video-switching-multi-decoder/) for near-zero-gap transitions using decoder pre-warming and early start. Refer to the [BrightSign model and series reference](https://docs.brightsign.biz/hardware/model-and-series-reference) to check your player's capabilities. - ## Configuration You can customize the following settings in `index.js` before deploying: - **`rootStoragePath`** - The root storage path on the BrightSign player (default: `/storage/sd`) - **`assetsFolder`** - The folder name containing your video files (default: `assets`) +- **`MULTI_DECODER`** - Set to `true` on players with multiple hardware decode pipelines to enable near-zero-gap transitions (default: `false`). See [Multi-decoder mode](#multi-decoder-mode) below. Example: ```javascript const rootStoragePath = '/storage/sd'; const assetsFolder = 'assets'; // Change this to use a different folder name +const MULTI_DECODER = false; // Set to true on XT/4K series players ``` If you change `assetsFolder` to a different name (e.g., `videos`), make sure to create that folder on your SD card and place your video files there instead. +## Multi-decoder mode + +On BrightSign players with multiple hardware decode pipelines (mid-to-high-end XT and 4K series), set `MULTI_DECODER = true` in `index.js` to enable two optimizations that further reduce the transition gap: + +1. **Decoder pre-warming** — after the hidden player buffers (`canplay`), it plays briefly then pauses at frame 0. This initializes the hardware decode pipeline so `play()` at switch time starts near-instantly. +2. **Early start** — the hidden player begins playing (muted, hidden) 0.5s before the visible video ends. By the time `ended` fires, the hidden player is already producing frames and the swap is immediate. + +If the early start does not complete in time, the swap falls back to awaiting the `playing` event, preventing a swap onto an unrendered frame. + +**Do not enable `MULTI_DECODER = true` on single-decoder hardware** — pre-warming and early start will compete with the active player's decoder and may cause stuttering. Leave `MULTI_DECODER = false` for the default single-decoder behavior. Refer to the [BrightSign model and series reference](https://docs.brightsign.biz/hardware/model-and-series-reference) to check your player's capabilities. + +| | `MULTI_DECODER = false` (default) | `MULTI_DECODER = true` | +|---|---|---| +| Hardware | Any BrightSign player | Multi-decoder required (XT, 4K) | +| Gap at transition | Small gap (decoder handoff) | Near-zero | +| Simultaneous decoding | No | Yes | + ## Deployment to BrightSign Player ### SD Card Structure @@ -74,4 +89,4 @@ The application will automatically: - **Supported video formats**: See [BrightSign video formats and codecs](https://docs.brightsign.biz/advanced/video-formats-and-codecs) for the list of containers, codecs, and profiles supported by each player series. - **Performance**: Playback smoothness and switching behavior may vary depending on player model, OS version, video mode (resolution/framerate), and the encoding of your source files. Adjust the code (e.g., preload behavior, number of preloaded players) to fit your specific use-case. -- **Multi-decoder hardware**: On mid-to-high-end players (XT, 4K series) that support simultaneous decode pipelines, see [seamless-video-switching-multi-decoder](../seamless-video-switching-multi-decoder/) for near-zero-gap transitions using decoder pre-warming and early start. +- **Early start threshold (multi-decoder mode)**: The 0.5s window in `armEarlyStart()` can be adjusted to trade content budget against transition reliability on your specific hardware and content type. diff --git a/examples/browser/seamless-video-switching/autorun.brs b/examples/browser/seamless-video-switching/autorun.brs index aa6e8a8..c8da76a 100644 --- a/examples/browser/seamless-video-switching/autorun.brs +++ b/examples/browser/seamless-video-switching/autorun.brs @@ -26,6 +26,8 @@ function CreateHTMLWidget(mp as object) as object ' Create HTML Widget config config = { + javascript_enabled: true, + brightsign_js_objects_enabled: true, nodejs_enabled: true, url: "file:///sd:/index.html", port: mp diff --git a/examples/browser/seamless-video-switching/index.js b/examples/browser/seamless-video-switching/index.js index 27617a3..3f5d117 100644 --- a/examples/browser/seamless-video-switching/index.js +++ b/examples/browser/seamless-video-switching/index.js @@ -4,6 +4,12 @@ const fs = require('fs'); const rootStoragePath = '/storage/sd'; const assetsFolder = 'assets'; +// Enable on players with multiple hardware decode pipelines (XT, 4K series) for +// near-zero-gap transitions via decoder pre-warming and early start. On single- +// decoder hardware, leave false — pre-warming would compete with the active +// player's decoder and may cause stuttering. See README for details. +const MULTI_DECODER = false; + // State tracking let currentVideoIndex = 0; let visibleVideoPlayer = 1; @@ -27,25 +33,67 @@ function preloadNextVideo() { hidden.muted = true; hidden.src = `${assetsFolder}/${videoFiles[nextVideoIndex]}`; hidden.load(); + + if (MULTI_DECODER) { + // Warm the decode pipeline so play() starts near-instantly at switch time + hidden.addEventListener('canplay', () => { + hidden.play().then(() => { hidden.pause(); hidden.currentTime = 0; }).catch(() => {}); + }, { once: true }); + } +} + +// Multi-decoder only: start hidden player 0.5s before end so it's already +// playing when the swap happens, eliminating decoder handoff gap. +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(); - // Defer visibility swap until the hidden player is actually playing - // to avoid revealing an unrendered frame - nextPlayer.addEventListener('playing', () => { - currentPlayer.pause(); - currentPlayer.muted = true; - currentPlayer.classList.add('hidden'); - nextPlayer.muted = false; - nextPlayer.classList.remove('hidden'); - visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; - preloadNextVideo(); - }, { once: true }); + if (MULTI_DECODER) { + // Early start may already have the hidden player running — swap instantly. + // Otherwise wait for `playing` to avoid revealing an unrendered frame. + 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 (!nextPlayer.paused) { + doSwap(); + } else { + nextPlayer.play().catch(e => console.error('Play error:', e)); + nextPlayer.addEventListener('playing', doSwap, { once: true }); + } + return; + } + // Single-decoder: start the hidden (preloaded) player and swap immediately. + nextPlayer.currentTime = 0; nextPlayer.play().catch(e => console.error('Play error:', e)); + + currentPlayer.pause(); + currentPlayer.muted = true; + currentPlayer.classList.add('hidden'); + + nextPlayer.muted = false; + nextPlayer.classList.remove('hidden'); + + visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1; + preloadNextVideo(); } async function main() { @@ -75,6 +123,7 @@ async function main() { player1.addEventListener('playing', () => { console.log('Player 1 playing'); preloadNextVideo(); + if (MULTI_DECODER) armEarlyStart(); }, { once: true }); } catch (e) { From 5bb4ce32986fd5e5c192ef38c8fa46e8bca15a19 Mon Sep 17 00:00:00 2001 From: jdmedlin1 Date: Wed, 24 Jun 2026 19:19:46 -0400 Subject: [PATCH 7/7] refactor(PE-1271): extract early-start lead time into EARLY_START_LEAD_SECONDS constant --- examples/browser/seamless-video-switching/README.md | 10 ++++++---- examples/browser/seamless-video-switching/index.js | 11 ++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/browser/seamless-video-switching/README.md b/examples/browser/seamless-video-switching/README.md index 33c68c2..04c127b 100644 --- a/examples/browser/seamless-video-switching/README.md +++ b/examples/browser/seamless-video-switching/README.md @@ -25,12 +25,14 @@ You can customize the following settings in `index.js` before deploying: - **`rootStoragePath`** - The root storage path on the BrightSign player (default: `/storage/sd`) - **`assetsFolder`** - The folder name containing your video files (default: `assets`) - **`MULTI_DECODER`** - Set to `true` on players with multiple hardware decode pipelines to enable near-zero-gap transitions (default: `false`). See [Multi-decoder mode](#multi-decoder-mode) below. +- **`EARLY_START_LEAD_SECONDS`** - Multi-decoder only. How many seconds before the visible video ends to start the hidden player (default: `0.5`). Raise if the swap reveals unrendered frames; lower for a tighter cut. Example: ```javascript const rootStoragePath = '/storage/sd'; -const assetsFolder = 'assets'; // Change this to use a different folder name -const MULTI_DECODER = false; // Set to true on XT/4K series players +const assetsFolder = 'assets'; // Change this to use a different folder name +const MULTI_DECODER = false; // Set to true on XT/4K series players +const EARLY_START_LEAD_SECONDS = 0.5; // Multi-decoder only ``` If you change `assetsFolder` to a different name (e.g., `videos`), make sure to create that folder on your SD card and place your video files there instead. @@ -40,7 +42,7 @@ If you change `assetsFolder` to a different name (e.g., `videos`), make sure to On BrightSign players with multiple hardware decode pipelines (mid-to-high-end XT and 4K series), set `MULTI_DECODER = true` in `index.js` to enable two optimizations that further reduce the transition gap: 1. **Decoder pre-warming** — after the hidden player buffers (`canplay`), it plays briefly then pauses at frame 0. This initializes the hardware decode pipeline so `play()` at switch time starts near-instantly. -2. **Early start** — the hidden player begins playing (muted, hidden) 0.5s before the visible video ends. By the time `ended` fires, the hidden player is already producing frames and the swap is immediate. +2. **Early start** — the hidden player begins playing (muted, hidden) `EARLY_START_LEAD_SECONDS` before the visible video ends (default 0.5s). By the time `ended` fires, the hidden player is already producing frames and the swap is immediate. If the early start does not complete in time, the swap falls back to awaiting the `playing` event, preventing a swap onto an unrendered frame. @@ -89,4 +91,4 @@ The application will automatically: - **Supported video formats**: See [BrightSign video formats and codecs](https://docs.brightsign.biz/advanced/video-formats-and-codecs) for the list of containers, codecs, and profiles supported by each player series. - **Performance**: Playback smoothness and switching behavior may vary depending on player model, OS version, video mode (resolution/framerate), and the encoding of your source files. Adjust the code (e.g., preload behavior, number of preloaded players) to fit your specific use-case. -- **Early start threshold (multi-decoder mode)**: The 0.5s window in `armEarlyStart()` can be adjusted to trade content budget against transition reliability on your specific hardware and content type. +- **Early start threshold (multi-decoder mode)**: Adjust `EARLY_START_LEAD_SECONDS` (default `0.5`) to trade content budget against transition reliability on your specific hardware and content type. diff --git a/examples/browser/seamless-video-switching/index.js b/examples/browser/seamless-video-switching/index.js index 3f5d117..128e32e 100644 --- a/examples/browser/seamless-video-switching/index.js +++ b/examples/browser/seamless-video-switching/index.js @@ -10,6 +10,11 @@ const assetsFolder = 'assets'; // player's decoder and may cause stuttering. See README for details. const MULTI_DECODER = false; +// Multi-decoder only: how many seconds before the visible video ends to start +// the hidden player. Trades content budget against transition reliability — +// raise if the swap is revealing unrendered frames, lower for a tighter cut. +const EARLY_START_LEAD_SECONDS = 0.5; + // State tracking let currentVideoIndex = 0; let visibleVideoPlayer = 1; @@ -42,12 +47,12 @@ function preloadNextVideo() { } } -// Multi-decoder only: start hidden player 0.5s before end so it's already -// playing when the swap happens, eliminating decoder handoff gap. +// Multi-decoder only: start hidden player EARLY_START_LEAD_SECONDS before end +// so it's already playing when the swap happens, eliminating decoder handoff gap. function armEarlyStart() { const { visible } = getPlayers(); visible.addEventListener('timeupdate', function onNearEnd() { - if (visible.duration - visible.currentTime <= 0.5) { + if (visible.duration - visible.currentTime <= EARLY_START_LEAD_SECONDS) { visible.removeEventListener('timeupdate', onNearEnd); getPlayers().hidden.play().catch(() => {}); }