UNPKG

shaka-player

Version:
356 lines (301 loc) 11 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Shaka Player demo, service worker. */ /** * The name of the cache for this version of the application. * This should be updated when old, unneeded application resources could be * cleaned up by a newer version of the application. * * @const {string} */ const CACHE_NAME = 'shaka-player-v3.0+'; /** * The prefix of all cache versions that belong to this application. * This is used to identify old caches to clean up. Must match CACHE_NAME * above. * * @const {string} */ const CACHE_NAME_PREFIX = 'shaka-player'; console.assert(CACHE_NAME.startsWith(CACHE_NAME_PREFIX), 'Cache name does not match prefix!'); /** * The maximum number of seconds to wait for an updated version of something * if we have a cached version we could use instead. * * @const {number} */ const NETWORK_TIMEOUT = 2; /** * An array of resources that MUST be cached to make the application * available offline. * * @const {!Array.<string>} */ const CRITICAL_RESOURCES = [ '.', // This resolves to the page. 'index.html', // Another way to access the page. 'app_manifest.json', 'shaka_logo_trans.png', 'load.js', '../dist/shaka-player.ui.js', '../dist/demo.compiled.js', '../dist/controls.css', '../dist/demo.css', // These files are required for the demo to include MDL. '../node_modules/material-design-lite/dist/material.min.js', // MDL modal dialogs are enabled by including these: '../node_modules/dialog-polyfill/dist/dialog-polyfill.js', // Datalist-like fields are enabled by including these: '../node_modules/awesomplete/awesomplete.min.js', // Tooltips are enabled by including these: '../node_modules/tippy.js/umd/index.min.js', '../node_modules/popper.js/dist/umd/popper.min.js', // PWA compatibility for iOS: '../node_modules/pwacompat/pwacompat.min.js', ].map(resolveRelativeUrl); /** * An array of resources that SHOULD be cached, but which are not critical. * * The application does not need to read these, so these can use the no-cors * flag and be cached as "opaque" resources. This is critical for the cast * sender SDK below. * * @const {!Array.<string>} */ const OPTIONAL_RESOURCES = [ // Optional graphics. Without these, the site won't be broken. 'favicon.ico', 'https://shaka-player-demo.appspot.com/assets/poster.jpg', 'https://shaka-player-demo.appspot.com/assets/audioOnly.gif', // The codem-isoboxer library for MSS support. '../node_modules/codem-isoboxer/dist/iso_boxer.min.js', // The cast sender SDK. 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js', // The IMA ads SDK. 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', 'https://imasdk.googleapis.com/js/sdkloader/ima3_dai.js', ].map(resolveRelativeUrl); /** * An array of URI prefixes. Matching resources SHOULD be cached whenever seen * and SHOULD be served from cache first without waiting for updated versions * from the network. * * @const {!Array.<string>} */ const CACHEABLE_URL_PREFIXES = [ // Translations should be cached. We don't know which ones the user will // want, so use this prefix. 'locales/', '../ui/locales/', // The various app logos should be cached, too. We don't know which ones the // browser will load, so use this prefix. 'app_logo_', // Google Web Fonts should be cached when first seen, without being explicitly // listed, and should be preferred from cache for speed. 'https://fonts.gstatic.com/', // Same goes for asset icons. 'https://storage.googleapis.com/shaka-asset-icons/', ].map(resolveRelativeUrl); /** * This constant is used to catch local resources which may be missing from the * set of cacheable URLs and prefixes above. * * @const {string} */ const LOCAL_BASE = resolveRelativeUrl('../'); /** * This event fires when the service worker is installed. * * @param {!InstallEvent} event */ function onInstall(event) { // Activate as soon as installation is complete. self.skipWaiting(); const preCacheApplication = async () => { const cache = await caches.open(CACHE_NAME); // Fetching these with addAll fails for CORS-restricted content, so we use // fetchAndCache with no-cors mode to work around it. // Optional resources: failure on these will NOT fail the Promise chain. // We will also not wait for them to be installed. for (const url of OPTIONAL_RESOURCES) { const request = new Request(url, {mode: 'no-cors'}); fetchAndCache(cache, request).catch(() => {}); } // Critical resources: failure on these will fail the Promise chain. // The installation will not be complete until these are all cached. const criticalFetches = []; for (const url of CRITICAL_RESOURCES) { const request = new Request(url, {mode: 'no-cors'}); criticalFetches.push(fetchAndCache(cache, request)); } return Promise.all(criticalFetches); }; event.waitUntil(preCacheApplication()); } /** * This event fires when the service worker is activated. * This can be after installation or upgrade. * * @param {!ExtendableEvent} event */ function onActivate(event) { // Delete old caches to save space. const dropOldCaches = async () => { const cacheNames = await caches.keys(); // Return true on all the caches we want to clean up. // Note that caches are shared across the origin, so only remove // caches we are sure we created. const cleanTheseUp = cacheNames.filter((cacheName) => cacheName.startsWith(CACHE_NAME_PREFIX) && cacheName != CACHE_NAME); const cleanUpPromises = cleanTheseUp.map((cacheName) => caches.delete(cacheName)); await Promise.all(cleanUpPromises); }; event.waitUntil(Promise.all([ dropOldCaches(), // makes this the active service worker for all open tabs clients.claim(), ])); } /** * This event fires when any resource is fetched. * This is where we can use the cache to respond offline. * * @param {!FetchEvent} event */ function onFetch(event) { // For some reason, on a page load, we get hash parameters in the URL for this // event. The hash should not be used when we do any of the lookups below. const url = event.request.url.split('#')[0]; // Make sure this is a request we should be handling in the first place. // If it's not, it's important to leave it alone and not call respondWith. let useCache = false; for (const prefix of CACHEABLE_URL_PREFIXES) { if (url.startsWith(prefix)) { useCache = true; break; } } // Now we need to check our resource lists. The list of prefixes above won't // cover everything that was installed initially, and those things still need // to be read from cache. So we check if this request URL matches one of // those lists. if (!useCache) { if (CRITICAL_RESOURCES.includes(url) || OPTIONAL_RESOURCES.includes(url)) { useCache = true; } } if (!useCache && url.startsWith(LOCAL_BASE)) { // If we have the correct resource lists above, then all local resources // should have useCache set to true by now. // Check to see if this request is coming from a compiled build of the demo, // and only log an error if this missing request is from a compiled build. // The check is async, and that's fine because we aren't handling this fetch // event anyway. (async () => { // This client represents the tab that made the request. const client = await clients.get(event.clientId); // This is the URL of that tab's main document. const urlHash = client.url.split('#')[1] || ''; const hashParameters = urlHash.split(';'); if (hashParameters.includes('build=compiled')) { console.error('Local resource missing!', url); } })(); } if (useCache) { event.respondWith(fetchCacheableResource(event.request)); } } /** * Fetch a cacheable resource. Decide whether to request from the network, * the cache, or both, and return the appropriate version of the resource. * * @param {!Request} request * @return {!Promise.<!Response>} */ async function fetchCacheableResource(request) { const cache = await caches.open(CACHE_NAME); const cachedResponse = await cache.match(request); if (!navigator.onLine) { // We are offline, and we know it. Just return the cached response, to // avoid a bunch of pointless errors in the JS console that will confuse // us developers. If there is no cached response, this will just be a // failed request. return cachedResponse; } if (cachedResponse) { // We have it in cache. Try to fetch a live version and update the cache, // but limit how long we will wait for the updated version. try { return await timeout(NETWORK_TIMEOUT, fetchAndCache(cache, request)); } catch (error) { // We tried to fetch a live version, but it either failed or took too // long. If it took too long, the fetch and cache operation will continue // in the background. In both cases, we should go ahead with a cached // version. return cachedResponse; } } else { // We should have this in cache, but we don't. Fetch and cache a fresh // copy and then return it. return fetchAndCache(cache, request); } } /** * Fetch the resource from the network, then store this new version in the * cache. * * @param {!Cache} cache * @param {!Request} request * @return {!Promise.<!Response>} */ async function fetchAndCache(cache, request) { const response = await fetch(request); cache.put(request, response.clone()); return response; } /** * Returns a Promise which is resolved only if |asyncProcess| is resolved, and * only if it is resolved in less than |seconds| seconds. * * If the returned Promise is resolved, it returns the same value as * |asyncProcess|. * * If |asyncProcess| fails, the returned Promise is rejected. * If |asyncProcess| takes too long, the returned Promise is rejected, but * |asyncProcess| is still allowed to complete. * * @param {number} seconds * @param {!Promise.<T>} asyncProcess * @return {!Promise.<T>} * @template T */ function timeout(seconds, asyncProcess) { return Promise.race([ asyncProcess, new Promise(((_, reject) => { setTimeout(reject, seconds * 1000); })), ]); } /** * @param {string} relativeUrl A URL which may be relative to this service * worker. * @return {string} The same URL converted to an absolute URL. */ function resolveRelativeUrl(relativeUrl) { // NOTE: This is the URL of the service worker, not the main HTML document. const baseUrl = location.href; return (new URL(relativeUrl, baseUrl)).href; } self.addEventListener('install', /** @type {function(!Event)} */(onInstall)); self.addEventListener('activate', /** @type {function(!Event)} */(onActivate)); self.addEventListener('fetch', /** @type {function(!Event)} */(onFetch));