UNPKG

shaka-player

Version:
288 lines (253 loc) 8.99 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.offline.DownloadManager'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.offline.DownloadProgressEstimator'); goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Destroyer'); goog.require('shaka.util.Error'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Pssh'); /** * 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 */ constructor(networkingEngine) { /** @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 {!shaka.util.Destroyer} */ this.destroyer_ = new shaka.util.Destroyer(() => { // Add a "catch" block to stop errors from being returned. return this.abortAll().catch(() => {}); }); /** * A list of callback functions to cancel any in-progress downloads. * * @private {!Array<function(): !Promise>} */ this.abortCallbacks_ = []; /** * 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_ = (progress, size) => {}; /** * A callback for when a segment has new PSSH data and we pass * on the initData to storage * * @private {function(!Uint8Array, string)} */ this.onInitData_ = (initData, systemId) => {}; /** @private {shaka.offline.DownloadProgressEstimator} */ this.estimator_ = new shaka.offline.DownloadProgressEstimator(); /** @private {boolean} */ this.aborted_ = false; } /** @override */ destroy() { return this.destroyer_.destroy(); } /** * @param {function(number, number)} onProgress * @param {function(!Uint8Array, string)} onInitData */ setCallbacks(onProgress, onInitData) { this.onProgress_ = onProgress; this.onInitData_ = onInitData; } /** * Aborts all in-progress downloads. * @return {!Promise} A promise that will resolve once the downloads are fully * aborted. */ abortAll() { this.aborted_ = true; const promises = this.abortCallbacks_.map((callback) => callback()); this.abortCallbacks_ = []; return Promise.all(promises); } /** * @return {boolean} */ isAborted() { return this.aborted_; } /** * Adds a byte length to the download estimate. * * @param {number} estimatedByteLength * @return {number} estimateId */ addDownloadEstimate(estimatedByteLength) { return this.estimator_.open(estimatedByteLength); } /** * 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} estimateId * @param {boolean} isInitSegment * @param {function(BufferSource):!Promise} onDownloaded * The callback for when this request has been downloaded. Downloading for * |group| will pause until the promise returned by |onDownloaded| resolves. * @return {!Promise} Resolved when this request is complete. */ queue(groupId, request, estimateId, isInitSegment, onDownloaded) { this.destroyer_.ensureNotDestroyed(); const group = this.groups_.get(groupId) || Promise.resolve(); // Add another download to the group. const newPromise = group.then(async () => { const response = await this.fetchSegment_(request); // Make sure we stop downloading if we have been destroyed. if (this.destroyer_.destroyed()) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.OPERATION_ABORTED); } // Update initData if (isInitSegment) { const segmentBytes = shaka.util.BufferUtils.toUint8(response); const pssh = new shaka.util.Pssh(segmentBytes); for (const key in pssh.data) { const index = Number(key); const data = pssh.data[index]; const systemId = pssh.systemIds[index]; this.onInitData_(data, systemId); } } // Update all our internal stats. this.estimator_.close(estimateId, response.byteLength); this.onProgress_( this.estimator_.getEstimatedProgress(), this.estimator_.getTotalDownloaded()); return onDownloaded(response); }); this.groups_.set(groupId, newPromise); return newPromise; } /** * Add already-downloaded data to 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 {!BufferSource} queueData * @param {number} estimateId * @param {boolean} isInitSegment * @param {function(BufferSource):!Promise} onDownloaded * The callback for when this request has been downloaded. Downloading for * |group| will pause until the promise returned by |onDownloaded| resolves. * @return {!Promise} Resolved when this request is complete. */ queueData(groupId, queueData, estimateId, isInitSegment, onDownloaded) { this.destroyer_.ensureNotDestroyed(); const group = this.groups_.get(groupId) || Promise.resolve(); // Add another download to the group. const newPromise = group.then(() => { // Make sure we stop downloading if we have been destroyed. if (this.destroyer_.destroyed()) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.OPERATION_ABORTED); } // Update initData if (isInitSegment) { const segmentBytes = shaka.util.BufferUtils.toUint8(queueData); const pssh = new shaka.util.Pssh(segmentBytes); for (const key in pssh.data) { const index = Number(key); const data = pssh.data[index]; const systemId = pssh.systemIds[index]; this.onInitData_(data, systemId); } } // Update all our internal stats. this.estimator_.close(estimateId, queueData.byteLength); this.onProgress_( this.estimator_.getEstimatedProgress(), this.estimator_.getTotalDownloaded()); return onDownloaded(queueData); }); this.groups_.set(groupId, newPromise); return newPromise; } /** * Add additional async work to the group work queue. * * @param {number} groupId * The group to add this group to. If the group does not exist, a new * group will be created. * @param {function():!Promise} callback * The callback for the async work. Downloading for this group will be * blocked until the Promise returned by |callback| resolves. * @return {!Promise} Resolved when this work is complete. */ queueWork(groupId, callback) { this.destroyer_.ensureNotDestroyed(); const group = this.groups_.get(groupId) || Promise.resolve(); const newPromise = group.then(async () => { await callback(); }); this.groups_.set(groupId, newPromise); return newPromise; } /** * 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.estimator_.getTotalDownloaded(); } /** * Download a segment and return the data in the response. * * @param {shaka.extern.Request} request * @return {!Promise<BufferSource>} * @private */ async fetchSegment_(request) { const type = shaka.net.NetworkingEngine.RequestType.SEGMENT; /** @type {!shaka.net.NetworkingEngine.PendingRequest} */ const action = this.networkingEngine_.request(type, request); const abortCallback = () => { return action.abort(); }; this.abortCallbacks_.push(abortCallback); const response = await action.promise; shaka.util.ArrayUtils.remove(this.abortCallbacks_, abortCallback); return response.data; } };