UNPKG

shaka-player

Version:
303 lines (264 loc) 9.72 kB
/** * @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.offline.OfflineUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.MapUtils'); /** * This manages downloading segments and notifying the app of progress. * * @param {shaka.offline.IStorageEngine} storageEngine * @param {!shaka.net.NetworkingEngine} netEngine * @param {shakaExtern.RetryParameters} retryParams * @param {shakaExtern.OfflineConfiguration} config * * @struct * @constructor * @implements {shaka.util.IDestroyable} */ shaka.offline.DownloadManager = function( storageEngine, netEngine, retryParams, config) { /** * @private {!Object.< * string, !Array.<shaka.offline.DownloadManager.Segment>>} */ this.segments_ = {}; /** * The IDs of the segments that have been stored for an in-progress * download(). This is used to cleanup in destroy(). * @private {!Array.<number>} */ this.storedSegments_ = []; /** @private {?shakaExtern.OfflineConfiguration} */ this.config_ = config; /** @private {shaka.offline.IStorageEngine} */ this.storageEngine_ = storageEngine; /** @private {shaka.net.NetworkingEngine} */ this.netEngine_ = netEngine; /** @private {?shakaExtern.RetryParameters} */ this.retryParams_ = retryParams; /** @private {?shakaExtern.ManifestDB} */ this.manifest_ = null; /** @private {Promise} */ this.promise_ = null; /** * The total number of bytes for segments that include a byte range. * @private {number} */ this.givenBytesTotal_ = 0; /** * The number of bytes downloaded for segments that include a byte range. * @private {number} */ this.givenBytesDownloaded_ = 0; /** * The total number of bytes estimated based on bandwidth for segments that * do not include a byte range. * @private {number} */ this.bandwidthBytesTotal_ = 0; /** * The estimated number of bytes downloaded for segments that do not have * a byte range. * @private {number} */ this.bandwidthBytesDownloaded_ = 0; }; /** * @typedef {{ * uris: !Array.<string>, * startByte: number, * endByte: ?number, * bandwidthSize: number, * segmentDb: shakaExtern.SegmentDataDB * }} * * @property {!Array.<string>} uris * The URIs to download the segment. * @property {number} startByte * The byte index the segment starts at. * @property {?number} endByte * The byte index the segment ends at, if present. * @property {number} bandwidthSize * The size of the segment as estimated by the bandwidth and segment duration. * @property {shakaExtern.SegmentDataDB} segmentDb * The data to store in the database. */ shaka.offline.DownloadManager.Segment; /** @override */ shaka.offline.DownloadManager.prototype.destroy = function() { var storage = this.storageEngine_; var segments = this.storedSegments_; var p = this.promise_ || Promise.resolve(); // Don't try to remove segments if there are none. That may trigger an error // in storage if the DB connection was never created. if (segments.length) { p = p.then(function() { return storage.removeKeys('segment', segments); }); } // Don't destroy() storageEngine since it is owned by Storage. this.segments_ = {}; this.storedSegments_ = []; this.config_ = null; this.storageEngine_ = null; this.netEngine_ = null; this.retryParams_ = null; this.manifest_ = null; this.promise_ = null; return p; }; /** * Adds a segment to the list to be downloaded. * * @param {string} type * @param {!shaka.media.SegmentReference|!shaka.media.InitSegmentReference} ref * @param {number} bandwidthSize * @param {shakaExtern.SegmentDataDB} segmentDb * The data to store in the database with the data. The |data| field of this * object will contain the downloaded data. */ shaka.offline.DownloadManager.prototype.addSegment = function( type, ref, bandwidthSize, segmentDb) { this.segments_[type] = this.segments_[type] || []; this.segments_[type].push({ uris: ref.getUris(), startByte: ref.startByte, endByte: ref.endByte, bandwidthSize: bandwidthSize, segmentDb: segmentDb }); }; /** * Downloads all the segments, stores them in the database, and stores the given * manifest object. * * @param {shakaExtern.ManifestDB} manifest * @return {!Promise} */ shaka.offline.DownloadManager.prototype.downloadAndStore = function(manifest) { var MapUtils = shaka.util.MapUtils; // Calculate progress estimates. this.givenBytesTotal_ = 0; this.givenBytesDownloaded_ = 0; this.bandwidthBytesTotal_ = 0; this.bandwidthBytesDownloaded_ = 0; MapUtils.values(this.segments_).forEach(function(segments) { segments.forEach(function(segment) { if (segment.endByte != null) this.givenBytesTotal_ += (segment.endByte - segment.startByte + 1); else this.bandwidthBytesTotal_ += segment.bandwidthSize; }.bind(this)); }.bind(this)); this.manifest_ = manifest; // Will be updated as we download for segments without a byte-range. this.manifest_.size = this.givenBytesTotal_; // Create separate download chains for different content types. This will // allow audio and video to be downloaded in parallel. var async = MapUtils.values(this.segments_).map(function(segments) { var i = 0; var downloadNext = (function() { if (!this.config_) { return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.OPERATION_ABORTED)); } if (i >= segments.length) return Promise.resolve(); var segment = segments[i++]; return this.downloadSegment_(segment).then(downloadNext); }.bind(this)); return downloadNext(); }.bind(this)); this.segments_ = {}; this.promise_ = Promise.all(async).then(function() { return this.storageEngine_.insert('manifest', manifest); }.bind(this)).then(function() { this.storedSegments_ = []; }.bind(this)); return this.promise_; }; /** * Downloads the given segment and calls the callback. * * @param {shaka.offline.DownloadManager.Segment} segment * @return {!Promise} * @private */ shaka.offline.DownloadManager.prototype.downloadSegment_ = function(segment) { goog.asserts.assert(this.retryParams_, 'DownloadManager must not be destroyed'); var type = shaka.net.NetworkingEngine.RequestType.SEGMENT; var request = shaka.net.NetworkingEngine.makeRequest(segment.uris, this.retryParams_); if (segment.startByte != 0 || segment.endByte != null) { var end = segment.endByte == null ? '' : segment.endByte; request.headers['Range'] = 'bytes=' + segment.startByte + '-' + end; } var byteCount; return this.netEngine_.request(type, request) .then(function(response) { if (!this.manifest_) { return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.OPERATION_ABORTED)); } byteCount = response.data.byteLength; this.storedSegments_.push(segment.segmentDb.key); segment.segmentDb.data = response.data; return this.storageEngine_.insert('segment', segment.segmentDb); }.bind(this)) .then(function() { // Do not hold onto the segment data after it has been stored in the // database. See https://github.com/google/shaka-player/issues/1167 // Note that an automated regression test for this is not feasible, so // anyone refactoring this code must take care to manually test memory // consumption as detailed in the issue comments on github. segment.segmentDb.data = null; if (!this.manifest_) { return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.OPERATION_ABORTED)); } if (segment.endByte == null) { // We didn't know the size, so it was an estimate. this.manifest_.size += byteCount; this.bandwidthBytesDownloaded_ += segment.bandwidthSize; } else { goog.asserts.assert( byteCount == (segment.endByte - segment.startByte + 1), 'Incorrect download size'); this.givenBytesDownloaded_ += byteCount; } this.updateProgress_(); }.bind(this)); }; /** * Calls the progress callback. * @private */ shaka.offline.DownloadManager.prototype.updateProgress_ = function() { var progress = (this.givenBytesDownloaded_ + this.bandwidthBytesDownloaded_) / (this.givenBytesTotal_ + this.bandwidthBytesTotal_); goog.asserts.assert(this.manifest_, 'Must not be destroyed'); var manifest = shaka.offline.OfflineUtils.getStoredContent(this.manifest_); this.config_.progressCallback(manifest, progress); };