diff --git a/examples/README.md b/examples/README.md index 9a2ee5d..187df84 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,11 @@ Note that some starter examples include the creation of a `brightsign-dumps` fol - **Location**: `examples/large-file-download` - **Features**: Downloads large files (multi-GB) to SD card on memory-constrained players without OOM or UI blocking. Uses Node.js streams with TCP-level backpressure via `roHtmlWidget`. +#### Seamless Video Switching Example + +- **Location**: `examples/seamless-video-switching` +- **Features**: HTML5 video player with dual video elements for seamless, gap-free transitions between videos. Preloads next video in the background for instant switching. + ### Node.js Examples #### Node Starter Example diff --git a/examples/seamless-video-switching/README.md b/examples/seamless-video-switching/README.md new file mode 100644 index 0000000..efdc8a9 --- /dev/null +++ b/examples/seamless-video-switching/README.md @@ -0,0 +1,67 @@ +# BrightSign Dual Video Player HTML5 Application - Seamless Playback + +> A seamless HTML5 video player application for BrightSign that provides gap-free video transitions using dual video elements with background preloading. + +## 🎯 Why Use This Approach? + +**This dual video player provides truly seamless, gap-free video playback** - essential for advertising and professional digital signage where any visible gap between videos is unacceptable. + +### The Dual Player Advantage: +- ✅ **Zero visible gaps** - No freeze frames or black screens between videos +- ✅ **Instant transitions** - Next video is preloaded and ready to play +- ✅ **No fade effects** - Clean cuts between videos without mixing content + +### How It Works: +1. Two video elements are layered on top of each other +2. While video 1 plays, video 2 loads the next file in the background +3. When video 1 ends, video 2 instantly becomes visible and starts playing +4. Video 1 (now hidden) loads the next file in the background +5. The cycle repeats for seamless continuous playback + +## 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`) + +Example: +```javascript +const rootStoragePath = '/storage/sd'; +const assetsFolder = 'assets'; // Change this to use a different folder name +``` + +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 + +## Deployment to BrightSign Player + +### SD Card Structure + +Your BrightSign player's SD card should have the following structure: + +``` +SD/ +├── autorun.brs (launches index.html) +├── index.html (loads index.js) +├── index.js (application logic) +└── assets/ (your video files) + ├── video1.mp4 + ├── video2.mp4 + └── video3.ts +``` + +### Deployment Steps + +1. Copy the following files to the root of your SD card: + - `autorun.brs` + - `index.html` + - `index.js` +2. Create an `assets/` folder on the SD card +3. Copy your video files into the `assets/` folder +4. Insert the SD card into your BrightSign player and power it on + +The application will automatically: +- Load video files from `/storage/sd/assets/` +- Sort them alphabetically +- Play them in sequence with seamless transitions +- Loop back to the first video after the last one finishes \ No newline at end of file diff --git a/examples/seamless-video-switching/architecture.md b/examples/seamless-video-switching/architecture.md new file mode 100644 index 0000000..22be016 --- /dev/null +++ b/examples/seamless-video-switching/architecture.md @@ -0,0 +1,61 @@ +# Architecture Diagram + +```mermaid +graph TD + Player["BrightSign Player"] + Autorun["autorun.brs
(BrightScript)"] + HTML["index.html
(Dual Video Elements)"] + Bundle["bundle.js
(Switching Logic)"] + Display["HDMI Display
(Video Output)"] + Assets[("assets/
(Video Files
.mp4, .ts)")] + Player1["Video Player 1
(Visible/Hidden)"] + Player2["Video Player 2
(Hidden/Visible)"] + + Player -->|"Boots & Launches"| Autorun + Autorun -->|"Creates roHtmlWidget
Loads HTML"| HTML + HTML -->|"Loads & Executes"| Bundle + Bundle -->|"Reads Video Files"| Assets + Bundle -->|"Controls Playback
& Visibility"| Player1 + Bundle -->|"Controls Playback
& Visibility"| Player2 + Player1 -->|"Renders Video"| Display + Player2 -->|"Renders Video"| Display + + style Player fill:#4a90e2,stroke:#333,stroke-width:2px,color:#fff + style Autorun fill:#e67e22,stroke:#333,stroke-width:2px,color:#fff + style HTML fill:#9b59b6,stroke:#333,stroke-width:2px,color:#fff + style Bundle fill:#7b68ee,stroke:#333,stroke-width:2px,color:#fff + style Display fill:#34495e,stroke:#333,stroke-width:2px,color:#fff + style Assets fill:#50c878,stroke:#333,stroke-width:2px,color:#fff + style Player1 fill:#e74c3c,stroke:#333,stroke-width:2px,color:#fff + style Player2 fill:#e74c3c,stroke:#333,stroke-width:2px,color:#fff +``` + +## Seamless Video Switching Flow + +1. **Initial Load**: Player 1 loads and plays the first video +2. **Preload**: While Player 1 plays, Player 2 preloads the next video (hidden) +3. **Switch Trigger**: When Player 1 ends, the switching sequence begins +4. **Start Hidden Player**: Player 2 starts playing (while still hidden) +5. **Wait for Playback**: Wait until Player 2 is actually playing +6. **Instant Transition**: Player 2 becomes visible, Player 1 becomes hidden +7. **Background Preload**: Player 1 (now hidden) preloads the next video +8. **Loop**: Repeat steps 3-7 indefinitely for continuous playback + +## Key Features + +- **Zero-Gap Transitions**: No black screens or freeze frames between videos +- **Dual Player Technique**: Two HTML5 video elements layered using absolute positioning +- **Background Preloading**: Next video is fully loaded before current video ends +- **Instant Visibility Toggle**: CSS class switching provides immediate visual transition +- **Alphabetical Playback**: Videos are sorted and played in alphabetical order +- **Infinite Loop**: Playlist automatically loops back to the first video + +## Legend + +- **Blue**: BrightSign Player +- **Orange**: BrightScript +- **Purple**: HTML/JS Application +- **Purple (Dark)**: JavaScript Logic +- **Dark Gray**: External Hardware +- **Green**: Video Files +- **Red**: Video Player Elements diff --git a/examples/seamless-video-switching/autorun.brs b/examples/seamless-video-switching/autorun.brs new file mode 100644 index 0000000..caa5576 --- /dev/null +++ b/examples/seamless-video-switching/autorun.brs @@ -0,0 +1,40 @@ +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 = { + javascript_enabled: true, + brightsign_js_objects_enabled: true, + 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/seamless-video-switching/index.html b/examples/seamless-video-switching/index.html new file mode 100644 index 0000000..bfb5760 --- /dev/null +++ b/examples/seamless-video-switching/index.html @@ -0,0 +1,59 @@ + + + + + + Seamless Video App + + + + +
+ + + +
+ + diff --git a/examples/seamless-video-switching/index.js b/examples/seamless-video-switching/index.js new file mode 100644 index 0000000..53a6cc6 --- /dev/null +++ b/examples/seamless-video-switching/index.js @@ -0,0 +1,114 @@ +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(); + + // 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.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(); +} diff --git a/package.json b/package.json index ab1dee4..36a7fef 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "examples/node-simple-server", "examples/node-starter", "examples/nodejs-web-app", + "examples/seamless-video-switching", "examples/self-signed-certs", "examples/send-plugin-message", "templates/**"