forked from shaka-project/shaka-player
-
Notifications
You must be signed in to change notification settings - Fork 1
/
service_worker.js
355 lines (301 loc) · 10.8 KB
/
service_worker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/*! @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 mux.js transmuxing library for MPEG-2 TS and CEA support.
'../node_modules/mux.js/dist/mux.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));