From aded252e40445f4c65565932fb400b35d9a18e59 Mon Sep 17 00:00:00 2001 From: Theodore Abshire Date: Fri, 13 Aug 2021 01:54:33 -0700 Subject: [PATCH] feat(offline): Make segment storage stateless. This refactors the storage mechanism so that the method that attaches a segment to the manifest is be a stateless async method, and no longer needs to be in the same session as the method that stored the manifest. This is the end of phase one of the work towards allowing Shaka Player to use background fetch to store assets offline. This change will allow a service worker to store the segments as they are downloaded, without having to keep the Shaka Player instance alive. Issue #879 Change-Id: I6a3545c57bacaf7229fe8c32669e88c6cc4e4138 --- build/types/core | 1 + docs/design/bg-fetch-after.gv | 29 +++ docs/design/bg-fetch-after.gv.png | Bin 0 -> 36821 bytes docs/design/bg-fetch-before.gv | 18 ++ docs/design/bg-fetch-before.gv.png | Bin 0 -> 23217 bytes docs/design/bg-fetch.md | 134 ++++++++++++ lib/offline/indexeddb/storage_mechanism.js | 2 +- lib/offline/storage.js | 236 ++++++++++++++------- lib/util/mutex.js | 52 +++++ 9 files changed, 400 insertions(+), 72 deletions(-) create mode 100644 docs/design/bg-fetch-after.gv create mode 100644 docs/design/bg-fetch-after.gv.png create mode 100644 docs/design/bg-fetch-before.gv create mode 100644 docs/design/bg-fetch-before.gv.png create mode 100644 docs/design/bg-fetch.md create mode 100644 lib/util/mutex.js diff --git a/build/types/core b/build/types/core index 5f19623958..30d61997d6 100644 --- a/build/types/core +++ b/build/types/core @@ -82,6 +82,7 @@ +../../lib/util/mp4_box_parsers.js +../../lib/util/mp4_parser.js +../../lib/util/multi_map.js ++../../lib/util/mutex.js +../../lib/util/networking.js +../../lib/util/object_utils.js +../../lib/util/operation_manager.js diff --git a/docs/design/bg-fetch-after.gv b/docs/design/bg-fetch-after.gv new file mode 100644 index 0000000000..4a6b8f196b --- /dev/null +++ b/docs/design/bg-fetch-after.gv @@ -0,0 +1,29 @@ +# Generate png with: dot -Tpng -O after.gv +digraph storage_after { + subgraph cluster_0 { + label="Shaka Player"; + parse[label="Download and parse manifest (parseManifest)"]; + drm[label="Make DRM engine and load keys (createDrmEngine)"] + filter[label="Filter manifest (filterManifest_)"]; + segments[label="Download segments (downloadSegments_)"]; + store[label="Store manifest (cell.addManifests)"]; + parse -> drm; + drm -> filter; + filter -> store; + store -> segments[label="BG Fetch Not Available"]; + } + subgraph cluster_1 { + label="Service Worker"; + bgSegments[label="Download segments in background (backgroundFetch.fetch)"] + store -> bgSegments[label="BG Fetch Available"]; + } + subgraph cluster_2 { + label="Shaka Player Static Methods"; + storeSeg[label="Store segments one-by-one (assignStreamToManifest)"] + remove[label="Clean up (cleanStoredManifest)"]; + segments -> remove[label="On Fail"]; + segments -> storeSeg; + bgSegments -> storeSeg; + bgSegments -> remove[label="On Fail"]; + } +} \ No newline at end of file diff --git a/docs/design/bg-fetch-after.gv.png b/docs/design/bg-fetch-after.gv.png new file mode 100644 index 0000000000000000000000000000000000000000..1a0d5d5c13fd58075faada90269a36fbe8ab2a81 GIT binary patch literal 36821 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 W#T#D~L|Zr^%>J_a6KCRb`~Lt4v!LPt literal 0 HcmV?d00001 diff --git a/docs/design/bg-fetch-before.gv b/docs/design/bg-fetch-before.gv new file mode 100644 index 0000000000..8da689b8cc --- /dev/null +++ b/docs/design/bg-fetch-before.gv @@ -0,0 +1,18 @@ +# Generate png with: dot -Tpng -O before.gv +digraph storage_before { + subgraph cluster_0 { + label="Shaka Player"; + parse[label="Download and parse manifest (parseManifest)"]; + drm[label="Make DRM engine and load keys (createDrmEngine)"] + filter[label="Filter manifest (filterManifest_)"]; + segments[label="Download and store segments (downloadManifest_)"]; + store[label="Store manifest (cell.addManifests)"]; + remove[label="Clean up (cell.removeSegments)"]; + parse -> drm; + drm -> filter; + filter -> segments; + segments -> store; + segments -> remove[label="On Fail"]; + store -> remove[label="On Fail"]; + } +} diff --git a/docs/design/bg-fetch-before.gv.png b/docs/design/bg-fetch-before.gv.png new file mode 100644 index 0000000000000000000000000000000000000000..832a340c8050901cd2d086cf8969e47a78890f5e GIT binary patch literal 23217 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 literal 0 HcmV?d00001 diff --git a/docs/design/bg-fetch.md b/docs/design/bg-fetch.md new file mode 100644 index 0000000000..15dca62e1c --- /dev/null +++ b/docs/design/bg-fetch.md @@ -0,0 +1,134 @@ +# Shaka Player Background Fetch Support + +last update: 2021-7-12 + +by: [theodab@google.com](mailto:theodab@google.com) + + +## Overview + +The feature of background fetch has been in Shaka Player’s backlog [since 2017]. +At the time it was added to the backlog, the feature was not quite ready for +use. Since then, it has matured, and now is something we could feasibly use, but +it has still been a low-priority feature. + +[since 2017]: https://github.com/google/shaka-player/issues/879 + +## Design Concept + +This design attempts to reuse existing code whenever possible, in order to +minimize the amount of new code that has to be tested. The code will be made in +two main stages: +1. Refactor the offline download process to change the order that the asset is +downloaded. The manifest should be downloaded and stored first, and then every +segment should be downloaded. As a segment is downloaded, it should be stored. +The code for storing a segment, in particular, should be broken out into an +exported static (e.g. stateless) function. +1. Modify the Shaka Player wrapper to add the appropriate background fetch event +listeners if the environment is detected to be a service worker, so that a +compiled Shaka Player bundle can be used as a service worker. If background +fetch is used, the segment downloading step should be passed to this service +worker. When each segment is downloaded, it should be passed to the static +storage functions added in stage 1. + +By restructuring the offline storage code in this way, switching between +foreground and background fetch will just be a matter of calling a different +segment-downloading function. In addition, it is possible that, in the future, a +plugin interface could be made for this. That probably won’t be necessary unless +another browser makes a competing API for downloading in the background (which +is admittedly a possibility, as background fetch [is not yet a W3C standard]). + +[is not yet a W3C standard]: https://wicg.github.io/background-fetch/ + +### Storage System Process: Before + +![Shaka storage system flow before](bg-fetch-before.gv.png) + + +### Storage System Process: After + +![Shaka storage system flow after](bg-fetch-after.gv.png) + + +## Implementation + +### Changes to shaka.offline.Storage + +1. Change createOfflineManifest_ to leave the storage indexes on the segments +null at first. With this change, downloadManifest_ will now only be downloading +the encryption keys (which cannot be downloaded via background fetch, as they +require request bodies). +1. Create a new step within store_, after the manifest is stored, called +“downloadSegments_” that makes a Set of SegmentReference objects that need to be +downloaded. + 1. We use SegmentReference objects in order to contain the URI, startByte, + and endByte. + 1. This change also means we will no longer need an internal cache for + downloaded segments, as they will be deduplicated by the use of a Set. +1. If background fetch is not available, downloadSegments_ will simply download +the segments from this set as before, and then once they are all downloaded, +pass them all to assignStreamsToManifest. +1. If background fetch is available, this set will be turned into an array, +Request objects should be made for the individual uris (with appropriate headers +applied), and then that array will be passed to the service worker with a +background fetch call. The service worker will then, after everything is +downloaded and stored, call assignStreamsToManifest. An estimate of the total +download size will need to be computed here, and padded to avoid premature +cancellation for inaccurate manifests. +1. Create a new public static method, assignStreamToManifest. This is a static +method that requires no internal state, so that the service worker can call it. +It stores the data provided, loads the manifest from storage, applies the +storage id of the data to the appropriate segments (based on uri), and then +stores the modified manifest. It should have a mutex over the part that loads +and changes the manifest, to keep one invocation from overriding the manifest +changes of another. It should have the following parameters: + 1. manifestStorageId + 1. uri + 1. data + 1. throwIfAbortedFn +1. Create a second public static method, cleanStoredManifest. This method is +meant to be called by the service worker in the instance of the fetch operation +being aborted, and will simply clear the manifest away. It will also clear any +segments that have been stored already. This also means we will no longer need +the segmentsFromStore_ array, which we had previously been using to un-store +after canceled or failed downloads. It should have the following parameters: + 1. manifestStorageId +1. When filling out shaka.extern.StoredContent entries for the list() method, +the storage system should be sure to set the offlineUri field to null if the +manifest is still “isIncomplete”, to mark that the asset has not yet finished +downloading. This will help developers detect that an asset is mid-download on +page load, so that they can set up progress indicators if they so wish. + + +### Service Worker Design + +1. This code should go in, or at least be loaded in, the wrapper code. This will +let us access Shaka Player methods inside the service worker, without having to +coordinate how to load a compiled Shaka Player bundle from a service worker; +the user can simply load a Shaka Player bundle as a service worker. +1. When the background fetch message is called (see [the documentation]), the +“id” field should be set to the storage id of the manifest, with an added prefix +of “Shaka-”. The API does not provide any field for custom data, but this value +still needs to be provided to the service worker somehow. Luckily, this is the +only extra data the service worker needs, so it can just be the id of the fetch +operation. + 1. When handling background fetch-related events, we can simply ignore any + event that does not start with the prefix. This will help prevent any + contamination with other service worker code from the developer. +1. As each segment is downloaded, the assignStreamToManifest method should be +called to store that data in the manifest. +1. If the download is canceled, call the cleanStoredManifest method, so that the +player doesn’t pollute indexedDb with unused segment data. +1. As a service worker is essentially just a collection of event listeners, one +can theoretically listen to the same event multiple times. This is relevant +because [a given scope] can only have a single service worker, so our service +worker code will have to be something that other people can load into their +existing service workers, if they have any. +1. Our system should use the message event to pass a specific identifying +message to the service worker, and the service worker will be expected to +respond with a specific response message. This way, we won’t mistake an +unrelated service worker for our own. + 1. This message can also be used to make sure the versions are the same. + +[the documentation]: https://developers.google.com/web/updates/2018/12/background-fetch#starting_a_background_fetch +[a given scope]: https://developers.google.com/web/fundamentals/primers/service-workers#register_a_service_worker \ No newline at end of file diff --git a/lib/offline/indexeddb/storage_mechanism.js b/lib/offline/indexeddb/storage_mechanism.js index ccc6f0a6c4..88a2fb0afa 100644 --- a/lib/offline/indexeddb/storage_mechanism.js +++ b/lib/offline/indexeddb/storage_mechanism.js @@ -315,7 +315,7 @@ shaka.offline.indexeddb.StorageMechanism = class { const del = window.indexedDB.deleteDatabase(name); del.onblocked = (event) => { - shaka.log.warning('Deleting', name, 'is being blocked'); + shaka.log.warning('Deleting', name, 'is being blocked', event); }; del.onsuccess = (event) => { p.resolve(); diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 5364bf30fb..93ec2fcd27 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -29,6 +29,7 @@ goog.require('shaka.util.Functional'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Iterables'); goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.Mutex'); goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); goog.require('shaka.util.StreamUtils'); @@ -94,14 +95,6 @@ shaka.offline.Storage = class { this.networkingEngine_ = new shaka.net.NetworkingEngine(); } - /** - * A list of segment ids for all the segments that were added during the - * current store. If the store fails or is aborted, these need to be - * removed from storage. - * @private {!Array.} - */ - this.segmentsFromStore_ = []; - /** * A list of open operations that are being performed by this instance of * |shaka.offline.Storage|. @@ -448,28 +441,14 @@ shaka.offline.Storage = class { return shaka.offline.StoredContentUtils.fromManifestDB( offlineUri, manifestDB); } catch (e) { - // If we did start saving some data, we need to remove it all to avoid - // wasting storage. However if the muxer did not manage to initialize, - // then we won't have an active cell to remove the segments from. - if (activeHandle) { - await activeHandle.cell.removeSegments( - this.segmentsFromStore_, () => {}); - } - if (manifestId != null) { - const uri = shaka.offline.OfflineUri.manifest( - activeHandle.path.mechanism, - activeHandle.path.cell, - manifestId); - await this.remove_(uri.toString()); + await shaka.offline.Storage.cleanStoredManifest(manifestId); } // If we already had an error, ignore this error to avoid hiding // the original error. throw drmError || e; } finally { - this.segmentsFromStore_ = []; - await muxer.destroy(); if (parser) { @@ -500,8 +479,14 @@ shaka.offline.Storage = class { async downloadSegments_( toDownload, manifestId, manifestDB, downloader, config, storage, manifest, drmEngine) { - /** @param {!Array.} toDownload */ - const download = (toDownload) => { + /** + * @param {!Array.} toDownload + * @param {boolean} updateDRM + */ + const download = async (toDownload, updateDRM) => { + const throwIfAbortedFn = () => { + this.ensureNotDestroyed_(); + }; for (const download of toDownload) { /** @param {?BufferSource} data */ let data; @@ -516,67 +501,173 @@ shaka.offline.Storage = class { request, estimateId, isInitSegment, onDownloaded); downloader.queueWork(download.groupId, async () => { goog.asserts.assert(data, 'We should have loaded data by now'); + goog.asserts.assert(data instanceof ArrayBuffer, + 'The data should be an ArrayBuffer'); const ref = /** @type {!shaka.media.SegmentReference} */ ( download.ref); - const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref); - - // Store the segment. - const ids = await storage.addSegments([{data: data}]); - this.ensureNotDestroyed_(); - this.segmentsFromStore_.push(ids[0]); - - // Attach the segment to the manifest. - let complete = true; - for (const stream of manifestDB.streams) { - for (const segment of stream.segments) { - if (segment.pendingSegmentRefId == idForRef) { - segment.dataKey = ids[0]; - // Now that the segment has been associated with the - // appropriate dataKey, the pendingSegmentRefId is no longer - // necessary. - segment.pendingSegmentRefId = undefined; - } - if (segment.pendingInitSegmentRefId == idForRef) { - segment.initSegmentKey = ids[0]; - // Now that the init segment has been associated with the - // appropriate initSegmentKey, the pendingInitSegmentRefId is - // no longer necessary. - segment.pendingInitSegmentRefId = undefined; - } - if (segment.pendingSegmentRefId) { - complete = false; - } - if (segment.pendingInitSegmentRefId) { - complete = false; - } - } - } - if (complete) { - manifestDB.isIncomplete = false; - } + manifestDB = (await shaka.offline.Storage.assignStreamToManifest( + manifestId, ref, {data}, throwIfAbortedFn)) || manifestDB; }); } + await downloader.waitToFinish(); + + if (updateDRM) { + // Re-store the manifest, to attach session IDs. + // These were (maybe) discovered inside the downloader; we can only add + // them now, at the end, since the manifestDB is in flux during the + // process of downloading and storing, and assignStreamToManifest does + // not know about the DRM engine. + this.ensureNotDestroyed_(); + this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config); + await storage.updateManifest(manifestId, manifestDB); + } }; - if (this.getManifestIsEncrypted_(manifest) && + const usingBgFetch = false; // TODO: Get. + + if (this.getManifestIsEncrypted_(manifest) && usingBgFetch && !this.getManifestIncludesInitData_(manifest)) { // Background fetch can't make DRM sessions, so if we have to get the // init data from the init segments, download those first before anything // else. - download(toDownload.filter((info) => info.isInitSegment)); + await download(toDownload.filter((info) => info.isInitSegment), true); + this.ensureNotDestroyed_(); toDownload = toDownload.filter((info) => !info.isInitSegment); } - download(toDownload); + if (!usingBgFetch) { + await download(toDownload, false); + this.ensureNotDestroyed_(); - // Re-store the manifest, to update the size and attach session IDs. - manifestDB.size = await downloader.waitToFinish(); - this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config); - goog.asserts.assert( - !manifestDB.isIncomplete, 'The manifest should be complete by now'); - await storage.updateManifest(manifestId, manifestDB); - this.ensureNotDestroyed_(); + goog.asserts.assert( + !manifestDB.isIncomplete, 'The manifest should be complete by now'); + } else { + // TODO: Send the request to the service worker. Don't await the result. + } + } + + /** + * Removes all of the contents for a given manifest, statelessly. + * + * @param {number} manifestId + * @return {!Promise} + */ + static async cleanStoredManifest(manifestId) { + const muxer = new shaka.offline.StorageMuxer(); + await muxer.init(); + const activeHandle = await muxer.getActive(); + const uri = shaka.offline.OfflineUri.manifest( + activeHandle.path.mechanism, + activeHandle.path.cell, + manifestId); + await muxer.destroy(); + const storage = new shaka.offline.Storage(); + await storage.remove(uri.toString()); + } + + /** + * Load the given manifest, modifies it by assigning the given data to the + * segments corresponding to "ref", then re-stores the manifest. + * The parts of this function that modify the manifest are protected by a + * mutex, to prevent race conditions; specifically, it prevents two parallel + * instances of this method from both loading the manifest into memory at the + * same time, which would result in the slower/later call overwriting the + * changes of the other. + * + * @param {number} manifestId + * @param {!shaka.media.SegmentReference} ref + * @param {shaka.extern.SegmentDataDB} data + * @param {function()} throwIfAbortedFn A function that should throw if the + * download has been aborted. + * @return {!Promise.} + */ + static async assignStreamToManifest(manifestId, ref, data, throwIfAbortedFn) { + /** @type {shaka.offline.StorageMuxer} */ + const muxer = new shaka.offline.StorageMuxer(); + + const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref); + let manifestUpdated = false; + let dataKey; + let activeHandle; + /** @type {!shaka.extern.ManifestDB} */ + let manifestDB; + + let mutexId = 0; + + try { + await muxer.init(); + activeHandle = await muxer.getActive(); + + // Store the data. + const dataKeys = await activeHandle.cell.addSegments([data]); + dataKey = dataKeys[0]; + throwIfAbortedFn(); + + // Acquire the mutex before accessing the manifest, since there could be + // multiple instances of this method running at once. + mutexId = await shaka.offline.Storage.mutex_.acquire(); + throwIfAbortedFn(); + + // Load the manifest. + const manifests = await activeHandle.cell.getManifests([manifestId]); + throwIfAbortedFn(); + manifestDB = manifests[0]; + + // Assign the stored data to the manifest. + let complete = true; + for (const stream of manifestDB.streams) { + for (const segment of stream.segments) { + if (segment.pendingSegmentRefId == idForRef) { + segment.dataKey = dataKey; + // Now that the segment has been associated with the appropriate + // dataKey, the pendingSegmentRefId is no longer necessary. + segment.pendingSegmentRefId = undefined; + } + if (segment.pendingInitSegmentRefId == idForRef) { + segment.initSegmentKey = dataKey; + // Now that the init segment has been associated with the + // appropriate initSegmentKey, the pendingInitSegmentRefId is no + // longer necessary. + segment.pendingInitSegmentRefId = undefined; + } + if (segment.pendingSegmentRefId) { + complete = false; + } + if (segment.pendingInitSegmentRefId) { + complete = false; + } + } + } + + // Update the size of the manifest. + manifestDB.size += data.data.byteLength; + + // Mark the manifest as complete, if all segments are downloaded. + if (complete) { + manifestDB.isIncomplete = false; + } + + // Re-store the manifest. + await activeHandle.cell.updateManifest(manifestId, manifestDB); + manifestUpdated = true; + throwIfAbortedFn(); + } catch (e) { + await shaka.offline.Storage.cleanStoredManifest(manifestId); + + if (activeHandle && !manifestUpdated && dataKey) { + // The cleanStoredManifest method will not "see" any segments that have + // been downloaded but not assigned to the manifest yet. So un-store + // them separately. + await activeHandle.cell.removeSegments([dataKey], (key) => {}); + } + + throw e; + } finally { + await muxer.destroy(); + shaka.offline.Storage.mutex_.release(mutexId); + } + return manifestDB; } /** @@ -1568,6 +1659,9 @@ shaka.offline.Storage = class { } }; +/** @private {!shaka.util.Mutex} */ +shaka.offline.Storage.mutex_ = new shaka.util.Mutex(); + shaka.offline.Storage.defaultSystemIds_ = new Map() .set('org.w3.clearkey', '1077efecc0b24d02ace33c1e52e2fb4b') .set('com.widevine.alpha', 'edef8ba979d64acea3c827dcd51d21ed') diff --git a/lib/util/mutex.js b/lib/util/mutex.js new file mode 100644 index 0000000000..afb22f9b95 --- /dev/null +++ b/lib/util/mutex.js @@ -0,0 +1,52 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.util.Mutex'); + + +/** + * @summary A simple mutex. + */ +shaka.util.Mutex = class { + /** Creates the mutex. */ + constructor() { + /** @private {!Array.} */ + this.waiting_ = []; + + /** @private {number} */ + this.nextMutexId_ = 0; + + /** @private {number} */ + this.acquiredMutexId_ = 0; + } + + /** @return {!Promise.} mutexId */ + async acquire() { + const mutexId = ++this.nextMutexId_; + if (!this.acquiredMutexId_) { + this.acquiredMutexId_ = mutexId; + } else { + await (new Promise((resolve, reject) => { + this.waiting_.push(() => { + this.acquiredMutexId_ = mutexId; + resolve(); + }); + })); + } + return mutexId; + } + + /** @param {number} mutexId */ + release(mutexId) { + if (mutexId == this.acquiredMutexId_) { + this.acquiredMutexId_ = 0; + if (this.waiting_.length > 0) { + const callback = this.waiting_.shift(); + callback(); + } + } + } +};