Skip to content
Merged
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
62 changes: 62 additions & 0 deletions examples/indexeddb-caching-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# IndexedDB Video Caching Example

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.

## File Structure

- `index.html`: Main HTML application with video player and caching logic
- `autorun.brs`: BrightSign autorun script which loads the HTML application

## Usage

1. Copy the `autorun.brs` and `index.html` files to the root of an SD card
2. Insert the SD card in the BrightSign player and power it on
3. The `autorun.brs` script will automatically launch the HTML application

## How It Works

1. **Database Setup**: Creates an IndexedDB database (`videos_db`) with an object store (`videos_os`) to store video blobs
2. **Playlist Loading**: Loads a predefined playlist of sample videos from Google's test video collection
3. **Smart Caching Strategy**:
- First video: Downloads and plays immediately, then starts background caching
- Subsequent videos: Plays from cache if available, otherwise downloads from network
- Background caching: Downloads remaining videos with 1-second delays between requests
4. **Automatic Playback**: Videos play continuously with automatic progression to the next video

## Configuration Options

### BrightSign Configuration (autorun.brs)
- `url`: Path to the HTML file. This can be modified to point to a remote server URL that serves the HTML file.
- `mouse_enabled`: Enable/disable mouse interaction (default: false)
- `javascript_enabled`: Enable/disable JavaScript (default: true)
- `nodejs_enabled`: Enable/disable Node.js (default: false)
- `storage_path`: Directory for local storage cache
- `storage_quota`: Maximum cache size

See the [roHtmlWidget](https://docs.brightsign.biz/developers/rohtmlwidget#23vJS) page for more details on configuration options.

### Video Configuration (index.html)
- Modify the `fetchPlaylist()` function to add your own video URLs
- Adjust the background caching delay by changing the `TIMEOUT_DELAY` value
- Customize video display properties in the `displayVideo()` function

## Error Handling

The application includes comprehensive error handling for:
- Network connectivity issues
- IndexedDB operation failures
- Duplicate cache entries (ConstraintError)
- Video loading and playback errors

## Performance Considerations

- **Storage**: Videos are stored as blobs in IndexedDB, which uses available disk space
- **Network**: Background caching uses staggered requests (1-second delays) to avoid overwhelming the network
- **Memory**: Video blobs are created using `URL.createObjectURL()` for efficient memory usage

## Troubleshooting

1. **Videos not caching**: Check console for IndexedDB errors
2. **Network errors**: Verify internet connectivity and video URLs
3. **Storage full**: Check available disk space and storage quota settings
4. **Playback issues**: Ensure video formats are supported by the player
40 changes: 40 additions & 0 deletions examples/indexeddb-caching-example/autorun.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Sub Main()
width = 1920
height = 1080

vidmode = CreateObject("roVideoMode")
if type(vidmode) = "roVideoMode"
width = vidmode.GetResX()
height = vidmode.GetResY()
end if

msgPort = CreateObject("roMessagePort")
rect = CreateObject("roRectangle", 0, 0, width, height)

'inspector disabled currently
'inspectorServer = {
' port: 3010
'}

config = {
url: "file:/SD:/index.html", ' you can use a server or website URL here - "https://example.com/index.html"
mouse_enabled: false, ' set to true to enable mouse/keyboard
'inspector_server: inspectorServer ' uncomment to enable inspector server
security_params: { websecurity: true }
javascript_enabled: true, ' Else set to false to disable JavaScript
nodejs_enabled: true, ' Else set to false to disable Node.js
storage_path: "/cache", ' Name of the directory for local storage cache
storage_quota: "2147483648", ' 2GB
port: msgPort,
}

html = CreateObject("roHtmlWidget", rect, config)
html.Show()

syslog = CreateObject("roSystemLog")

while true
event = wait(0, msgPort)
syslog.sendline("@@ type(event)="+ type(event))
end while
End Sub
189 changes: 189 additions & 0 deletions examples/indexeddb-caching-example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<!DOCTYPE html>
<head>
<script>
let db;
let currentIndex = 0;
let videos;
let TIMEOUT_DELAY = 1000; // Delay between cache requests in milliseconds

function fetchPlaylist () {
// Examples taken from https://gist.github.com/SeunghoonBaek/f35e0fd3db80bf55c2707cae5d0f7184
videos = [
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4'
];
}

// Check if a video is already cached in IndexedDB
function getVideoCacheResult(fileName) {
return new Promise((resolve) => {
const objectStore = db.transaction('videos_os').objectStore('videos_os');
const request = objectStore.get(fileName);
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
resolve(null); // If the video is not found, resolve with null
};
});
}

function cacheVideo(mp4Blob, name) {
const objectStore = db.transaction(['videos_os'], 'readwrite').objectStore('videos_os');
const record = {
mp4: mp4Blob,
name: name
}
const request = objectStore.add(record);
request.onsuccess = function() {
console.log(`Successfully cached: ${name}`);
}
request.onerror = function() {
// If the error is due to a duplicate key, it's not a real error
if (request.error.name === 'ConstraintError') {
console.log(`Video ${name} already exists in cache`);
} else {
console.error(`Error caching ${name}:`, request.error);
}
}
}

// Cache a video in the background without displaying it
async function cacheVideoInBackground(videoUrl) {
const fileName = videoUrl.split('/').pop();
try {
const cached = await getVideoCacheResult(fileName);
if (cached) {
console.log(`${fileName} is already cached`);
return;
}
const response = await fetch(videoUrl);
const blob = await response.blob();
cacheVideo(blob, fileName);
} catch(error) {
console.error(`Error caching ${fileName}:`, error.message);
}
}

// Start background caching for videos that aren't cached yet
function startBackgroundCaching() {
console.log('Starting background caching for videos');
for (let i = currentIndex + 1; i < videos.length; i++) {
const videoUrl = videos[i];
// Add a small delay between cache requests to avoid overwhelming the network
setTimeout(() => {
cacheVideoInBackground(videoUrl);
}, (i - currentIndex) * TIMEOUT_DELAY);
}
}

async function fetchVideoFromNetwork(videoUrl) {
try {
const fileName = videoUrl.split('/').pop();
console.log('fetching video from network: ', fileName);
const response = await fetch(videoUrl);
const blob = await response.blob();
cacheVideo(blob, fileName);
displayVideo(blob, fileName);

// Start background caching if this is the first video
if (currentIndex === 0) {
startBackgroundCaching();
}
} catch(error) {
console.error('Error fetching video from network:', error.message);
}
}

function displayVideo(mp4Blob, fileName) {
const mp4URL = URL.createObjectURL(mp4Blob);

// Create DOM elements to embed video in the page
const article = document.createElement('article');
const video = document.createElement('video');
video.autoplay = true;
video.style="width:100%; height:100%;";

video.onplay = function () {
console.log("*** videoPlay: " + fileName + " ***");
};
video.onended = function () {
console.log("*** videoEnded; playing next video ***");
currentIndex++;
playNextVideo();
};

const section = document.querySelector('section');
section.innerHTML = ''; // Clear previous content
section.appendChild(article);
article.appendChild(video);

const source1 = document.createElement('source');
source1.src = mp4URL;
source1.type = 'video/mp4';
video.appendChild(source1);
}

async function playNextVideo() {
if (currentIndex >= videos.length) {
console.log('All videos have been played; looping back to the start');
currentIndex = 0; // Reset index to loop through videos again
}

const videoUrl = videos[currentIndex];
const fileName = videoUrl.split('/').pop();

// Check if the video is already in IndexedDB
const cacheResult = await getVideoCacheResult(fileName);
if (cacheResult) {
console.log(`Playing ${fileName} from IDB`);
displayVideo(cacheResult.mp4, cacheResult.name);

// Start background caching if this is the first video and it's from cache
if (currentIndex === 0) {
startBackgroundCaching();
}
} else {
console.log(`Video ${fileName} not found in cache, fetching from network`);
fetchVideoFromNetwork(videoUrl);
}
}

window.onload = function() {
console.log("PAGE LOADED");
// Open (or create) the database
const request = window.indexedDB.open('videos_db', 1);

request.onerror = function() {
console.log('Database failed to open');
};

request.onsuccess = function() {
console.log('Database opened successfully');
db = request.result;

fetchPlaylist();
currentIndex = 0;
playNextVideo();
};

// Setup the database tables if this has not already been done
request.onupgradeneeded = function(e) {
const db = e.target.result;
const objectStore = db.createObjectStore('videos_os', { keyPath: 'name' });

// Define what data items the objectStore will contain
objectStore.createIndex('mp4', 'mp4', { unique: false });
console.log('Database setup complete');
};
};
</script>
</head>
<html>
<body style="width:100%; height:100%; color: white; text-align: center;">
<section>
<h2 style="margin-top:20%">Loading playlist ...</h2>
</section>
</body>
</html>