/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.DownloadManager');
goog.require('goog.asserts');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');
/**
* This manages downloading segments.
*
* @implements {shaka.util.IDestroyable}
* @final
*/
shaka.offline.DownloadManager = class {
/**
* Create a new download manager. It will use (but not own) |networkingEngine|
* and call |onProgress| after each download.
*
* @param {!shaka.net.NetworkingEngine} networkingEngine
* @param {function(number, number)} onProgress
*/
constructor(networkingEngine, onProgress) {
/** @private {shaka.net.NetworkingEngine} */
this.networkingEngine_ = networkingEngine;
/**
* We group downloads. Within each group, the requests are executed in
* series. Between groups, the requests are executed in parallel. We store
* the promise chain that is doing the work.
*
* @private {!Map.<number, !Promise>}
*/
this.groups_ = new Map();
/** @private {boolean} */
this.destroyed_ = false;
/**
* A callback for when a segment has been downloaded. The first parameter
* is the progress of all segments, a number between 0.0 (0% complete) and
* 1.0 (100% complete). The second parameter is the total number of bytes
* that have been downloaded.
*
* @private {function(number, number)}
*/
this.onProgress_ = onProgress;
/**
* We track progress using the estimated size (not the actual size) since
* the denominator (current / total) will be based on estimates.
*
* @private {number}
*/
this.downloadedEstimatedBytes_ = 0;
/**
* When we queue a segment, the estimated size is added to this value. This
* is used to track progress (downloaded / expected).
*
* @private {number}
*/
this.expectedEstimatedBytes_ = 0;
/**
* When a segment is downloaded, the actual size of the segment is added to
* this value. We use this to know how large the final asset is.
*
* @private {number}
*/
this.downloadedBytes_ = 0;
}
/** @override */
destroy() {
// Setting this will cause the promise chains to stop.
this.destroyed_ = true;
// Append no-ops so that we ensure that no errors escape |destroy|.
return Promise.all(this.groups_.values()).then(() => {}, () => {});
}
/**
* Add a request to be downloaded as part of a group.
*
* @param {number} groupId
* The group to add this segment to. If the group does not exist, a new
* group will be created.
* @param {shaka.extern.Request} request
* @param {number} estimatedByteLength
* @param {function(!ArrayBuffer):!Promise} onDownloaded
* The callback for when this request has been downloaded. Downloading for
* |group| will pause until the promise returned by |onDownloaded| resolves.
*/
queue(groupId, request, estimatedByteLength, onDownloaded) {
goog.asserts.assert(
!this.destroyed_,
'Do not call |queue| after |destroy|');
// Update our estimate.
this.expectedEstimatedBytes_ += estimatedByteLength;
const group = this.groups_.get(groupId) || Promise.resolve();
// Add another download to the group.
this.groups_.set(groupId, group.then(async () => {
const response = await this.fetchSegment_(request);
// Make sure we stop downloading if we have been destroyed.
if (this.destroyed_) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED);
}
// Update all our internal stats.
this.downloadedEstimatedBytes_ += estimatedByteLength;
this.downloadedBytes_ += response.byteLength;
const progress =
this.expectedEstimatedBytes_ ?
this.downloadedEstimatedBytes_ / this.expectedEstimatedBytes_ :
0;
this.onProgress_(progress, this.downloadedBytes_);
return onDownloaded(response);
}));
}
/**
* Get a promise that will resolve when all currently queued downloads have
* finished.
*
* @return {!Promise.<number>}
*/
async waitToFinish() {
await Promise.all(this.groups_.values());
return this.downloadedBytes_;
}
/**
* Download a segment and return the data in the response.
*
* @param {shaka.extern.Request} request
* @return {!Promise.<!ArrayBuffer>}
* @private
*/
async fetchSegment_(request) {
const type = shaka.net.NetworkingEngine.RequestType.SEGMENT;
const action = this.networkingEngine_.request(type, request);
const response = await action.promise;
return response.data;
}
};