Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions examples/seamless-video-switching/README.md
Original file line number Diff line number Diff line change
@@ -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
61 changes: 61 additions & 0 deletions examples/seamless-video-switching/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Architecture Diagram

```mermaid
graph TD
Player["BrightSign Player"]
Autorun["autorun.brs<br/>(BrightScript)"]
HTML["index.html<br/>(Dual Video Elements)"]
Bundle["index.js<br/>(Switching Logic)"]
Display["HDMI Display<br/>(Video Output)"]
Assets[("assets/<br/>(Video Files<br/>.mp4, .ts)")]
Player1["Video Player 1<br/>(Visible/Hidden)"]
Player2["Video Player 2<br/>(Hidden/Visible)"]

Player -->|"Boots & Launches"| Autorun
Autorun -->|"Creates roHtmlWidget<br/>Loads HTML"| HTML
HTML -->|"Loads & Executes"| Bundle
Bundle -->|"Reads Video Files"| Assets
Bundle -->|"Controls Playback<br/>& Visibility"| Player1
Bundle -->|"Controls Playback<br/>& 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
38 changes: 38 additions & 0 deletions examples/seamless-video-switching/autorun.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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
59 changes: 59 additions & 0 deletions examples/seamless-video-switching/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Seamless Video App</title>
<style>
body,
html {
margin: 0;
height: 100%;
padding: 0;
height: 100%;
overflow: hidden;
}

body {
font-family: Arial, sans-serif;
color: white;
}

#video-container {
width: 100%;
height: 100%;
}

#video-player {
width: 100%;
height: 100%;
}

.hidden {
display: none;
}

#video-player-1,
#video-player-2 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

video {
background: black;
}

</style>
<script defer src="index.js"></script>
</head>
<body>
<div id="video-container">
<video id="video-player-1" preload="auto" autoplay></video>
<video id="video-player-2" class="hidden" preload="auto"></video>
</video>
</div>
</body>
</html>
114 changes: 114 additions & 0 deletions examples/seamless-video-switching/index.js
Original file line number Diff line number Diff line change
@@ -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();
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"
Expand Down
Loading
Loading