BrightSign Asset Pool Example

This script shows how the BrightSign Asset Pool can be used from JavaScript. It works from both NodeJS and Chromium. It is not intended as a full application and it doesn’t do anything with the assets that are downloaded.

// MIT License // // Copyright (c) 2020 BrightSign // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // Customise these settings for your environment const storagePath = "/storage/sd/"; const poolPath = storagePath + "examplePool"; const serverPrefix = "https://github.com/brightsign/javascript-assetpoolfetcher/blob/video-training/"; const fs = require('fs'); const AssetPool = require("@brightsign/assetpool"); const AssetPoolFiles = require("@brightsign/assetpoolfiles"); const AssetFetcher = require("@brightsign/assetfetcher"); const AssetRealizer = require("@brightsign/assetrealizer"); // Create the asset collection array to be used by the rest of the // program. The asset collection will usually come either directly or // indirectly from a server somewhere rather than being generated in // code like this. function makeAssetCollection() { // Assets must have a name and a link. Everything else is // optional. You can add your own properties if required. For // example, the "osUpdate" property here is only used by this // script. let videoAsset1 = { name: "brightlink.mp4", hash: { method: "SHA256", hex: "82e5e800ff4b2608ae996b6c9fce78c0d84b82cba0da270251846324f48fb076" }, link: serverPrefix + "brightlink%201min.mp4?raw=true", size: 17219989, }; let videoAsset2 = { name: "CMScontrol.mp4", hash: { method: "SHA512", hex: "d476296b114fda612917b1568a9f7ffe6f54f3f927b2d1ecc932816e912619eeef980e1adc16d8b1d7914d3f172cd6a2ef17172ea3251ddcdbb3d151faa6818f" }, link: serverPrefix + "CMS%2BControl%201%20min.mp4?raw=true", size: 5633877, }; // We don't want to risk actually changing the OS version on the // BrightSign running this example cript, so this is not a real // BrightSignOS file. It will be realized into the root of the // storage device, but the OS will fail to find the expected // header and rename it to placeholder.bsfw_invalid. let osUpdateAsset = { name: "placeholder.bsfw", hash: { method: "SHA1", hex: "20e89c9ba0491590e3c34bc704171c0c02e643c3" }, link: serverPrefix + "placeholder.bsfw?raw=true", size: 43, osUpdate: true, }; let assetCollection = [ videoAsset1, videoAsset2, osUpdateAsset, ]; return assetCollection; } // Convert a progress event to a useful string for reporting function progressString(event) { if (event.currentFileTotal === undefined) { // If the size of the asset was not specified in the asset collection, then the total size may not be reported // during the fetch. return event.currentFileTransferred.toString() + " of unknown"; } else { return event.currentFileTransferred.toString() + " of " + event.currentFileTotal.toString() + " " + (100*event.currentFileTransferred / event.currentFileTotal).toFixed(0) + "%"; } } // Download any assets that aren't already in the pool into the pool // whilst reporting progress. async function fetchAssets(assetPool, assetCollection) { console.log("Fetch: " + JSON.stringify(assetCollection.map(asset => asset.name))); let assetFetcher = new AssetFetcher(assetPool); assetFetcher.addEventListener("fileevent", (event) => { // This is called each time the fetcher has finished trying to // download an asset, whether successful or not. It is not // called for any assets that are already in the pool. console.log("ASSET [" + (event.index + 1).toString() + "] " + event.fileName + " complete: " + event.responseCode.toString() + " " + event.error); }); assetFetcher.addEventListener("progressevent", (event) => { // This is called at approximately the progress interval // specified in the options to indicate how far through the // download console.log("ASSET [" + (event.index + 1).toString() + "/" + event.total.toString() + "] " + event.fileName + " progress: " + progressString(event)); }); const fetchOptions = { // receive asset progress events about every five seconds. progressInterval: 5, // try to download each asset three times before giving up. fileRetryCount: 3, // Give up if we fail to download at least 1024 bytes in each // ten second period. minimumTransferRate: { bytesPerSecond: 1024, periodInSeconds: 10 }, }; try { await assetFetcher.start(assetCollection, fetchOptions); } catch (err) { console.log("FETCH FAILED: " + err.message); throw(err); } } // In order to make use of an asset from the pool you need to look up // its pool filename so you can refer to it there. async function useAssets(assetPool, assetCollection) { let files = new AssetPoolFiles(assetPool, assetCollection); for (const fileName of [ 'brightlink.mp4', 'CMScontrol.mp4' ]) { const path = await files.getPath(fileName); console.log("Asset " + fileName + " is at " + path); } } // Some files need to appear in the filesystem outside the pool. For // example, BrightSignOS update files must be written to the root of // a storage device for them to be found. Realizing will copy files, // so can be slow on large files. async function realizeAssets(assetPool, assetCollection) { let realizer = new AssetRealizer(assetPool, storagePath); // We only want to realize the files that we have to const assetsToRealize = assetCollection.filter(asset => asset.osUpdate); console.log("Realize: " + JSON.stringify(assetsToRealize.map(asset => asset.name))); await realizer.realize(assetsToRealize); } function ensureDirectoryExists(path) { try { fs.mkdirSync(path); } catch (err) { if (err.code != 'EEXIST') throw(err); } } function exceptionToString(err) { if (err instanceof Error) return err.name + ":" + err.message; else if (typeof(err) === "string") return err; else return JSON.stringify(err); } async function runExample() { console.log("Start"); ensureDirectoryExists(poolPath); // Only one AssetPool instance should be created for a given pool // path. Having multiple instances risks them disagreeing over // which assets are protected during pruning. let assetPool = new AssetPool(poolPath); // Don't let the pool grow any larger than 500MiB await assetPool.setMaximumPoolSize(500 * 1024 * 1024); // Don't let free space on the storage device fall below 100MiB await assetPool.reserveStorage(100 * 1024 * 1024); const assetCollection = makeAssetCollection(); // We need to stop the fetcher from pruning any of the assets we // currently care about in order to make space for fetching new // assets or realizing existing ones. Assets are protected until // the AssetPool instance is destroyed or unprotectAssets is // called for the same name. await assetPool.protectAssets("collection1", assetCollection); await fetchAssets(assetPool, assetCollection); // In this case any failure to fetch the assets will cause // fetchAssets to have thrown an exception, so we won't get this // far. However, in a larger script it may be more convenient to // call areAssetsReady to determine whether the asset collection // is ready for use. if (await assetPool.areAssetsReady(assetCollection)) { await useAssets(assetPool, assetCollection); await realizeAssets(assetPool, assetCollection); } else { console.log("Assets were not downloaded successfully"); } } runExample() .then(() => { console.log("Complete"); process.exit(0); }) .catch((err) => { console.log("Failed " + exceptionToString(err)); process.exit(1); });