Skip to content

Commit 0ecc004

Browse files
committed
feat: PE-1271: dev-cookbook: Add example for seamless video switching
1 parent 637f0b1 commit 0ecc004

7 files changed

Lines changed: 344 additions & 0 deletions

File tree

examples/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Note that some starter examples include the creation of a `brightsign-dumps` fol
2828
- **Location**: `examples/large-file-download`
2929
- **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`.
3030

31+
#### Seamless Video Switching Example
32+
33+
- **Location**: `examples/seamless-video-switching`
34+
- **Features**: HTML5 video player with dual video elements for seamless, gap-free transitions between videos. Preloads next video in the background for instant switching.
35+
3136
### Node.js Examples
3237

3338
#### Node Starter Example
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# BrightSign Dual Video Player HTML5 Application - Seamless Playback
2+
3+
> A seamless HTML5 video player application for BrightSign that provides gap-free video transitions using dual video elements with background preloading.
4+
5+
## 🎯 Why Use This Approach?
6+
7+
**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.
8+
9+
### The Dual Player Advantage:
10+
-**Zero visible gaps** - No freeze frames or black screens between videos
11+
-**Instant transitions** - Next video is preloaded and ready to play
12+
-**No fade effects** - Clean cuts between videos without mixing content
13+
14+
### How It Works:
15+
1. Two video elements are layered on top of each other
16+
2. While video 1 plays, video 2 loads the next file in the background
17+
3. When video 1 ends, video 2 instantly becomes visible and starts playing
18+
4. Video 1 (now hidden) loads the next file in the background
19+
5. The cycle repeats for seamless continuous playback
20+
21+
## Configuration
22+
23+
You can customize the following settings in `index.js` before deploying:
24+
25+
- **`rootStoragePath`** - The root storage path on the BrightSign player (default: `/storage/sd`)
26+
- **`assetsFolder`** - The folder name containing your video files (default: `assets`)
27+
28+
Example:
29+
```javascript
30+
const rootStoragePath = '/storage/sd';
31+
const assetsFolder = 'assets'; // Change this to use a different folder name
32+
```
33+
34+
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
35+
36+
## Deployment to BrightSign Player
37+
38+
### SD Card Structure
39+
40+
Your BrightSign player's SD card should have the following structure:
41+
42+
```
43+
SD/
44+
├── autorun.brs (launches index.html)
45+
├── index.html (loads index.js)
46+
├── index.js (application logic)
47+
└── assets/ (your video files)
48+
├── video1.mp4
49+
├── video2.mp4
50+
└── video3.ts
51+
```
52+
53+
### Deployment Steps
54+
55+
1. Copy the following files to the root of your SD card:
56+
- `autorun.brs`
57+
- `index.html`
58+
- `index.js`
59+
2. Create an `assets/` folder on the SD card
60+
3. Copy your video files into the `assets/` folder
61+
4. Insert the SD card into your BrightSign player and power it on
62+
63+
The application will automatically:
64+
- Load video files from `/storage/sd/assets/`
65+
- Sort them alphabetically
66+
- Play them in sequence with seamless transitions
67+
- Loop back to the first video after the last one finishes
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Architecture Diagram
2+
3+
```mermaid
4+
graph TD
5+
Player["BrightSign Player"]
6+
Autorun["autorun.brs<br/>(BrightScript)"]
7+
HTML["index.html<br/>(Dual Video Elements)"]
8+
Bundle["bundle.js<br/>(Switching Logic)"]
9+
Display["HDMI Display<br/>(Video Output)"]
10+
Assets[("assets/<br/>(Video Files<br/>.mp4, .ts)")]
11+
Player1["Video Player 1<br/>(Visible/Hidden)"]
12+
Player2["Video Player 2<br/>(Hidden/Visible)"]
13+
14+
Player -->|"Boots & Launches"| Autorun
15+
Autorun -->|"Creates roHtmlWidget<br/>Loads HTML"| HTML
16+
HTML -->|"Loads & Executes"| Bundle
17+
Bundle -->|"Reads Video Files"| Assets
18+
Bundle -->|"Controls Playback<br/>& Visibility"| Player1
19+
Bundle -->|"Controls Playback<br/>& Visibility"| Player2
20+
Player1 -->|"Renders Video"| Display
21+
Player2 -->|"Renders Video"| Display
22+
23+
style Player fill:#4a90e2,stroke:#333,stroke-width:2px,color:#fff
24+
style Autorun fill:#e67e22,stroke:#333,stroke-width:2px,color:#fff
25+
style HTML fill:#9b59b6,stroke:#333,stroke-width:2px,color:#fff
26+
style Bundle fill:#7b68ee,stroke:#333,stroke-width:2px,color:#fff
27+
style Display fill:#34495e,stroke:#333,stroke-width:2px,color:#fff
28+
style Assets fill:#50c878,stroke:#333,stroke-width:2px,color:#fff
29+
style Player1 fill:#e74c3c,stroke:#333,stroke-width:2px,color:#fff
30+
style Player2 fill:#e74c3c,stroke:#333,stroke-width:2px,color:#fff
31+
```
32+
33+
## Seamless Video Switching Flow
34+
35+
1. **Initial Load**: Player 1 loads and plays the first video
36+
2. **Preload**: While Player 1 plays, Player 2 preloads the next video (hidden)
37+
3. **Switch Trigger**: When Player 1 ends, the switching sequence begins
38+
4. **Start Hidden Player**: Player 2 starts playing (while still hidden)
39+
5. **Wait for Playback**: Wait until Player 2 is actually playing
40+
6. **Instant Transition**: Player 2 becomes visible, Player 1 becomes hidden
41+
7. **Background Preload**: Player 1 (now hidden) preloads the next video
42+
8. **Loop**: Repeat steps 3-7 indefinitely for continuous playback
43+
44+
## Key Features
45+
46+
- **Zero-Gap Transitions**: No black screens or freeze frames between videos
47+
- **Dual Player Technique**: Two HTML5 video elements layered using absolute positioning
48+
- **Background Preloading**: Next video is fully loaded before current video ends
49+
- **Instant Visibility Toggle**: CSS class switching provides immediate visual transition
50+
- **Alphabetical Playback**: Videos are sorted and played in alphabetical order
51+
- **Infinite Loop**: Playlist automatically loops back to the first video
52+
53+
## Legend
54+
55+
- **Blue**: BrightSign Player
56+
- **Orange**: BrightScript
57+
- **Purple**: HTML/JS Application
58+
- **Purple (Dark)**: JavaScript Logic
59+
- **Dark Gray**: External Hardware
60+
- **Green**: Video Files
61+
- **Red**: Video Player Elements
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
function main()
2+
mp = CreateObject("roMessagePort")
3+
4+
' Create HTML Widget
5+
widget = CreateHTMLWidget(mp)
6+
widget.Show()
7+
8+
'Event Loop
9+
while true
10+
msg = wait(0, mp)
11+
print "msg received - type=";type(msg)
12+
if type(msg) = "roHtmlWidgetEvent" then
13+
print "msg: ";msg
14+
end if
15+
end while
16+
17+
end function
18+
19+
function CreateHTMLWidget(mp as object) as object
20+
' Get Screen Resolution
21+
vidmode = CreateObject("roVideoMode")
22+
width = vidmode.GetResX()
23+
height = vidmode.GetResY()
24+
25+
r = CreateObject("roRectangle", 0, 0, width, height)
26+
27+
' Create HTML Widget config
28+
config = {
29+
nodejs_enabled: true,
30+
url: "file:///sd:/index.html",
31+
port: mp
32+
}
33+
34+
' Create HTML Widget
35+
h = CreateObject("roHtmlWidget", r, config)
36+
return h
37+
38+
end function
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Seamless Video App</title>
7+
<style>
8+
body,
9+
html {
10+
margin: 0;
11+
height: 100%;
12+
padding: 0;
13+
height: 100%;
14+
overflow: hidden;
15+
}
16+
17+
body {
18+
font-family: Arial, sans-serif;
19+
color: white;
20+
}
21+
22+
#video-container {
23+
width: 100%;
24+
height: 100%;
25+
}
26+
27+
#video-player {
28+
width: 100%;
29+
height: 100%;
30+
}
31+
32+
.hidden {
33+
display: none;
34+
}
35+
36+
#video-player-1,
37+
#video-player-2 {
38+
position: absolute;
39+
top: 0;
40+
left: 0;
41+
width: 100%;
42+
height: 100%;
43+
}
44+
45+
video {
46+
background: black;
47+
}
48+
49+
</style>
50+
</head>
51+
<body>
52+
<div id="video-container">
53+
<video id="video-player-1" preload="auto" autoplay></video>
54+
<video id="video-player-2" class="hidden" preload="auto"></video>
55+
</video>
56+
</div>
57+
</body>
58+
</html>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const fs = require('fs');
2+
3+
// Configuration - edit these values as needed
4+
const rootStoragePath = '/storage/sd';
5+
const assetsFolder = 'assets';
6+
7+
// State tracking
8+
let currentVideoIndex = 0;
9+
let visibleVideoPlayer = 1;
10+
let videoFiles = [];
11+
12+
// Helper function to get player element by number
13+
function getPlayer(playerNumber) {
14+
return document.getElementById(`video-player-${playerNumber}`);
15+
}
16+
17+
// Helper function to get the currently visible and hidden players
18+
function getPlayers() {
19+
const visible = getPlayer(visibleVideoPlayer);
20+
const hidden = getPlayer(visibleVideoPlayer === 1 ? 2 : 1);
21+
return { visible, hidden };
22+
}
23+
24+
async function main() {
25+
console.log('main() - App ready!');
26+
27+
try {
28+
// Load and filter video files
29+
videoFiles = fs.readdirSync(`${rootStoragePath}/${assetsFolder}`);
30+
videoFiles = videoFiles.filter(filename => !filename.startsWith('.'));
31+
videoFiles.sort();
32+
33+
// console.log('Video files:', videoFiles);
34+
35+
if (videoFiles.length === 0) {
36+
console.error(`No video files found in ${rootStoragePath}/${assetsFolder}`);
37+
return;
38+
}
39+
40+
const player1 = getPlayer(1);
41+
const player2 = getPlayer(2);
42+
43+
// Initialize first video
44+
player1.src = `${assetsFolder}/${videoFiles[0]}`;
45+
player1.currentTime = 0;
46+
player1.muted = false; // Ensure audio is enabled for first player
47+
player2.muted = true; // Mute the second player initially
48+
console.log('Player 1 loaded:', videoFiles[0]);
49+
50+
// Set up video ended listeners for both players
51+
player1.addEventListener('ended', () => switchToNextVideo());
52+
player2.addEventListener('ended', () => switchToNextVideo());
53+
54+
// Preload next video once first video starts playing
55+
player1.addEventListener('playing', () => {
56+
console.log('Player 1 playing');
57+
preloadNextVideo();
58+
}, { once: true });
59+
60+
} catch (e) {
61+
console.error('Error in main():', e);
62+
}
63+
}
64+
65+
// Preload the next video in the hidden player
66+
function preloadNextVideo() {
67+
const nextVideoIndex = (currentVideoIndex + 1) % videoFiles.length;
68+
const { hidden } = getPlayers();
69+
const filePath = `${assetsFolder}/${videoFiles[nextVideoIndex]}`;
70+
71+
console.log(`Preloading: ${videoFiles[nextVideoIndex]}`);
72+
73+
// Reset and load the next video
74+
hidden.pause();
75+
hidden.currentTime = 0;
76+
hidden.muted = true; // Ensure hidden player is muted
77+
hidden.src = filePath;
78+
hidden.load();
79+
}
80+
81+
// Switch to the next video seamlessly
82+
function switchToNextVideo() {
83+
currentVideoIndex = (currentVideoIndex + 1) % videoFiles.length;
84+
const { visible: currentPlayer, hidden: nextPlayer } = getPlayers();
85+
86+
console.log(`Switching to: ${videoFiles[currentVideoIndex]}`);
87+
88+
// Ensure next player starts from beginning
89+
nextPlayer.currentTime = 0;
90+
91+
// Start playing the next video (while still hidden)
92+
nextPlayer.play().catch(e => console.error('Play error:', e));
93+
94+
// Switch visibility and audio immediately - the video is already preloaded
95+
currentPlayer.pause();
96+
currentPlayer.muted = true; // Mute the outgoing player
97+
currentPlayer.classList.add('hidden');
98+
99+
nextPlayer.muted = false; // Unmute the incoming player
100+
nextPlayer.classList.remove('hidden');
101+
102+
// Toggle which player is visible
103+
visibleVideoPlayer = visibleVideoPlayer === 1 ? 2 : 1;
104+
105+
// Preload the next video in the now-hidden player
106+
preloadNextVideo();
107+
}
108+
109+
// Call main when DOM is ready
110+
if (document.readyState === 'loading') {
111+
document.addEventListener('DOMContentLoaded', main);
112+
} else {
113+
main();
114+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"examples/node-simple-server",
1414
"examples/node-starter",
1515
"examples/nodejs-web-app",
16+
"examples/seamless-video-switching",
1617
"examples/self-signed-certs",
1718
"examples/send-plugin-message",
1819
"templates/**"

0 commit comments

Comments
 (0)