Source: lib/offline/download_manager.js

/**
 * @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;
  }
};