Skip to content

Commit 1589be3

Browse files
committed
feat: PE-964: [dev-cookbook] Add indexeddb example
- Add example for showcasing how to use indexedDB to cache videos and playback in a loop
1 parent 8257261 commit 1589be3

File tree

3 files changed

+291
-0
lines changed

3 files changed

+291
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# IndexedDB Video Caching Example
2+
3+
This example demonstrates how to use IndexedDB for caching video content in a BrightSign HTML5 application. The application creates a video playlist that intelligently caches videos in the background for smooth playback.
4+
5+
## File Structure
6+
7+
- `index.html`: Main HTML application with video player and caching logic
8+
- `autorun.brs`: BrightSign autorun script which loads the HTML application
9+
10+
## Usage
11+
12+
1. Copy the `autorun.brs` and `index.html` files to the root of an SD card
13+
2. Insert the SD card in the BrightSign player and power it on
14+
3. The `autorun.brs` script will automatically launch the HTML application
15+
16+
## How It Works
17+
18+
1. **Database Setup**: Creates an IndexedDB database (`videos_db`) with an object store (`videos_os`) to store video blobs
19+
2. **Playlist Loading**: Loads a predefined playlist of sample videos from Google's test video collection
20+
3. **Smart Caching Strategy**:
21+
- First video: Downloads and plays immediately, then starts background caching
22+
- Subsequent videos: Plays from cache if available, otherwise downloads from network
23+
- Background caching: Downloads remaining videos with 1-second delays between requests
24+
4. **Automatic Playback**: Videos play continuously with automatic progression to the next video
25+
26+
## Configuration Options
27+
28+
### BrightSign Configuration (autorun.brs)
29+
- `url`: Path to the HTML file. This can be modified to point to a remote server URL that serves the HTML file.
30+
- `mouse_enabled`: Enable/disable mouse interaction (default: false)
31+
- `javascript_enabled`: Enable/disable JavaScript (default: true)
32+
- `nodejs_enabled`: Enable/disable Node.js (default: false)
33+
- `storage_path`: Directory for local storage cache
34+
- `storage_quota`: Maximum cache size
35+
36+
See the [roHtmlWidget](https://docs.brightsign.biz/developers/rohtmlwidget#23vJS) page for more details on configuration options.
37+
38+
### Video Configuration (index.html)
39+
- Modify the `fetchPlaylist()` function to add your own video URLs
40+
- Adjust the background caching delay by changing the `TIMEOUT_DELAY` value
41+
- Customize video display properties in the `displayVideo()` function
42+
43+
## Error Handling
44+
45+
The application includes comprehensive error handling for:
46+
- Network connectivity issues
47+
- IndexedDB operation failures
48+
- Duplicate cache entries (ConstraintError)
49+
- Video loading and playback errors
50+
51+
## Performance Considerations
52+
53+
- **Storage**: Videos are stored as blobs in IndexedDB, which uses available disk space
54+
- **Network**: Background caching uses staggered requests (1-second delays) to avoid overwhelming the network
55+
- **Memory**: Video blobs are created using `URL.createObjectURL()` for efficient memory usage
56+
57+
## Troubleshooting
58+
59+
1. **Videos not caching**: Check console for IndexedDB errors
60+
2. **Network errors**: Verify internet connectivity and video URLs
61+
3. **Storage full**: Check available disk space and storage quota settings
62+
4. **Playback issues**: Ensure video formats are supported by the player
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Sub Main()
2+
width = 1920
3+
height = 1080
4+
5+
vidmode = CreateObject("roVideoMode")
6+
if type(vidmode) = "roVideoMode"
7+
width = vidmode.GetResX()
8+
height = vidmode.GetResY()
9+
end if
10+
11+
msgPort = CreateObject("roMessagePort")
12+
rect = CreateObject("roRectangle", 0, 0, width, height)
13+
14+
'inspector disabled currrently
15+
'inspectorServer = {
16+
' port: 3010
17+
'}
18+
19+
config = {
20+
url: "file:/SD:/index.html", ' you can use a server or website URL here - "https://example.com/index.html"
21+
mouse_enabled: false, ' set to true to enable mouse/keyboard
22+
'inspector_server: inspectorServer ' uncomment to enable inspector server
23+
security_params: { websecurity: true }
24+
javascript_enabled: true, ' set to false to disable JavaScript
25+
nodejs_enabled: true, ' set to false to disable Node.js
26+
storage_path: "/cache", ' Name of the directory for local storage cache
27+
storage_quota: "2147483648", ' 2GB
28+
port: msgPort,
29+
}
30+
31+
html = CreateObject("roHtmlWidget", rect, config)
32+
html.Show()
33+
34+
syslog = CreateObject("roSystemLog")
35+
36+
while true
37+
event = wait(0, msgPort)
38+
syslog.sendline("@@ type(event)="+ type(event))
39+
end while
40+
End Sub
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<script>
4+
let db;
5+
let currentIndex = 0;
6+
let videos;
7+
let TIMEOUT_DELAY = 1000; // Delay between cache requests in milliseconds
8+
9+
function fetchPlaylist () {
10+
// Examples taken from https://gist.github.com/SeunghoonBaek/f35e0fd3db80bf55c2707cae5d0f7184
11+
videos = [
12+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
13+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
14+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4'
15+
];
16+
}
17+
18+
// Check if a video is already cached in IndexedDB
19+
function getVideoCacheResult(fileName) {
20+
return new Promise((resolve) => {
21+
const objectStore = db.transaction('videos_os').objectStore('videos_os');
22+
const request = objectStore.get(fileName);
23+
request.onsuccess = function() {
24+
resolve(request.result);
25+
};
26+
request.onerror = function() {
27+
resolve(null); // If the video is not found, resolve with null
28+
};
29+
});
30+
}
31+
32+
function cacheVideo(mp4Blob, name) {
33+
const objectStore = db.transaction(['videos_os'], 'readwrite').objectStore('videos_os');
34+
const record = {
35+
mp4: mp4Blob,
36+
name: name
37+
}
38+
const request = objectStore.add(record);
39+
request.onsuccess = function() {
40+
console.log(`Successfully cached: ${name}`);
41+
}
42+
request.onerror = function() {
43+
// If the error is due to a duplicate key, it's not a real error
44+
if (request.error.name === 'ConstraintError') {
45+
console.log(`Video ${name} already exists in cache`);
46+
} else {
47+
console.error(`Error caching ${name}:`, request.error);
48+
}
49+
}
50+
}
51+
52+
// Cache a video in the background without displaying it
53+
async function cacheVideoInBackground(videoUrl) {
54+
const fileName = videoUrl.split('/').pop();
55+
try {
56+
const cached = await getVideoCacheResult(fileName);
57+
if (cached) {
58+
console.log(`${fileName} is already cached`);
59+
return;
60+
}
61+
const response = await fetch(videoUrl);
62+
const blob = await response.blob();
63+
cacheVideo(blob, fileName);
64+
} catch(error) {
65+
console.error(`Error caching ${fileName}:`, error.message);
66+
}
67+
}
68+
69+
// Start background caching for videos that aren't cached yet
70+
function startBackgroundCaching() {
71+
console.log('Starting background caching for videos');
72+
for (let i = currentIndex + 1; i < videos.length; i++) {
73+
const videoUrl = videos[i];
74+
// Add a small delay between cache requests to avoid overwhelming the network
75+
setTimeout(() => {
76+
cacheVideoInBackground(videoUrl);
77+
}, (i - currentIndex) * TIMEOUT_DELAY);
78+
}
79+
}
80+
81+
async function fetchVideoFromNetwork(videoUrl) {
82+
try {
83+
const fileName = videoUrl.split('/').pop();
84+
console.log('fetching video from network: ', fileName);
85+
const response = await fetch(videoUrl);
86+
const blob = await response.blob();
87+
cacheVideo(blob, fileName);
88+
displayVideo(blob, fileName);
89+
90+
// Start background caching if this is the first video
91+
if (currentIndex === 0) {
92+
startBackgroundCaching();
93+
}
94+
} catch(error) {
95+
console.error('Error fetching video from network:', error.message);
96+
}
97+
}
98+
99+
function displayVideo(mp4Blob, fileName) {
100+
const mp4URL = URL.createObjectURL(mp4Blob);
101+
102+
// Create DOM elements to embed video in the page
103+
const article = document.createElement('article');
104+
const video = document.createElement('video');
105+
video.autoplay = true;
106+
video.style="width:100%; height:100%;";
107+
108+
video.onplay = function () {
109+
console.log("*** videoPlay: " + fileName + " ***");
110+
};
111+
video.onended = function () {
112+
console.log("*** videoEnded; playing next video ***");
113+
currentIndex++;
114+
playNextVideo();
115+
};
116+
117+
const section = document.querySelector('section');
118+
section.innerHTML = ''; // Clear previous content
119+
section.appendChild(article);
120+
article.appendChild(video);
121+
122+
const source1 = document.createElement('source');
123+
source1.src = mp4URL;
124+
source1.type = 'video/mp4';
125+
video.appendChild(source1);
126+
}
127+
128+
async function playNextVideo() {
129+
if (currentIndex >= videos.length) {
130+
console.log('All videos have been played; looping back to the start');
131+
currentIndex = 0; // Reset index to loop through videos again
132+
}
133+
134+
const videoUrl = videos[currentIndex];
135+
const fileName = videoUrl.split('/').pop();
136+
137+
// Check if the video is already in IndexedDB
138+
const cacheResult = await getVideoCacheResult(fileName);
139+
if (cacheResult) {
140+
console.log(`Playing ${fileName} from IDB`);
141+
displayVideo(cacheResult.mp4, cacheResult.name);
142+
143+
// Start background caching if this is the first video and it's from cache
144+
if (currentIndex === 0) {
145+
startBackgroundCaching();
146+
}
147+
} else {
148+
console.log(`Video ${fileName} not found in cache, fetching from network`);
149+
fetchVideoFromNetwork(videoUrl);
150+
}
151+
}
152+
153+
window.onload = function() {
154+
console.log("PAGE LOADED");
155+
// Open (or create) the database
156+
const request = window.indexedDB.open('videos_db', 1);
157+
158+
request.onerror = function() {
159+
console.log('Database failed to open');
160+
};
161+
162+
request.onsuccess = function() {
163+
console.log('Database opened succesfully');
164+
db = request.result;
165+
166+
fetchPlaylist();
167+
currentIndex = 0;
168+
playNextVideo();
169+
};
170+
171+
// Setup the database tables if this has not already been done
172+
request.onupgradeneeded = function(e) {
173+
const db = e.target.result;
174+
const objectStore = db.createObjectStore('videos_os', { keyPath: 'name', autoIncrement: true });
175+
176+
// Define what data items the objectStore will contain
177+
objectStore.createIndex('mp4', 'mp4', { unique: false });
178+
console.log('Database setup complete');
179+
};
180+
};
181+
</script>
182+
</head>
183+
<html>
184+
<body style="width:100%; height:100%; color: white; text-align: center;">
185+
<section>
186+
<h2 style="margin-top:20%">Loading playlist ...</h2>
187+
</section>
188+
</body>
189+
</html>

0 commit comments

Comments
 (0)