Source: lib/offline/download_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.offline.DownloadManager');
  7. goog.require('shaka.net.NetworkingEngine');
  8. goog.require('shaka.offline.DownloadProgressEstimator');
  9. goog.require('shaka.util.ArrayUtils');
  10. goog.require('shaka.util.BufferUtils');
  11. goog.require('shaka.util.Destroyer');
  12. goog.require('shaka.util.Error');
  13. goog.require('shaka.util.IDestroyable');
  14. goog.require('shaka.util.Pssh');
  15. /**
  16. * This manages downloading segments.
  17. *
  18. * @implements {shaka.util.IDestroyable}
  19. * @final
  20. */
  21. shaka.offline.DownloadManager = class {
  22. /**
  23. * Create a new download manager. It will use (but not own) |networkingEngine|
  24. * and call |onProgress| after each download.
  25. *
  26. * @param {!shaka.net.NetworkingEngine} networkingEngine
  27. */
  28. constructor(networkingEngine) {
  29. /** @private {shaka.net.NetworkingEngine} */
  30. this.networkingEngine_ = networkingEngine;
  31. /**
  32. * We group downloads. Within each group, the requests are executed in
  33. * series. Between groups, the requests are executed in parallel. We store
  34. * the promise chain that is doing the work.
  35. *
  36. * @private {!Map.<number, !Promise>}
  37. */
  38. this.groups_ = new Map();
  39. /** @private {!shaka.util.Destroyer} */
  40. this.destroyer_ = new shaka.util.Destroyer(() => {
  41. // Add a "catch" block to stop errors from being returned.
  42. return this.abortAll().catch(() => {});
  43. });
  44. /**
  45. * A list of callback functions to cancel any in-progress downloads.
  46. *
  47. * @private {!Array.<function():!Promise>}
  48. */
  49. this.abortCallbacks_ = [];
  50. /**
  51. * A callback for when a segment has been downloaded. The first parameter
  52. * is the progress of all segments, a number between 0.0 (0% complete) and
  53. * 1.0 (100% complete). The second parameter is the total number of bytes
  54. * that have been downloaded.
  55. *
  56. * @private {function(number, number)}
  57. */
  58. this.onProgress_ = (progress, size) => {};
  59. /**
  60. * A callback for when a segment has new PSSH data and we pass
  61. * on the initData to storage
  62. *
  63. * @private {function(!Uint8Array, string)}
  64. */
  65. this.onInitData_ = (initData, systemId) => {};
  66. /** @private {shaka.offline.DownloadProgressEstimator} */
  67. this.estimator_ = new shaka.offline.DownloadProgressEstimator();
  68. }
  69. /** @override */
  70. destroy() {
  71. return this.destroyer_.destroy();
  72. }
  73. /**
  74. * @param {function(number, number)} onProgress
  75. * @param {function(!Uint8Array, string)} onInitData
  76. */
  77. setCallbacks(onProgress, onInitData) {
  78. this.onProgress_ = onProgress;
  79. this.onInitData_ = onInitData;
  80. }
  81. /**
  82. * Aborts all in-progress downloads.
  83. * @return {!Promise} A promise that will resolve once the downloads are fully
  84. * aborted.
  85. */
  86. abortAll() {
  87. const promises = this.abortCallbacks_.map((callback) => callback());
  88. this.abortCallbacks_ = [];
  89. return Promise.all(promises);
  90. }
  91. /**
  92. * Adds a byte length to the download estimate.
  93. *
  94. * @param {number} estimatedByteLength
  95. * @return {number} estimateId
  96. */
  97. addDownloadEstimate(estimatedByteLength) {
  98. return this.estimator_.open(estimatedByteLength);
  99. }
  100. /**
  101. * Add a request to be downloaded as part of a group.
  102. *
  103. * @param {number} groupId
  104. * The group to add this segment to. If the group does not exist, a new
  105. * group will be created.
  106. * @param {shaka.extern.Request} request
  107. * @param {number} estimateId
  108. * @param {boolean} isInitSegment
  109. * @param {function(BufferSource):!Promise} onDownloaded
  110. * The callback for when this request has been downloaded. Downloading for
  111. * |group| will pause until the promise returned by |onDownloaded| resolves.
  112. * @return {!Promise} Resolved when this request is complete.
  113. */
  114. queue(groupId, request, estimateId, isInitSegment, onDownloaded) {
  115. this.destroyer_.ensureNotDestroyed();
  116. const group = this.groups_.get(groupId) || Promise.resolve();
  117. // Add another download to the group.
  118. const newPromise = group.then(async () => {
  119. const response = await this.fetchSegment_(request);
  120. // Make sure we stop downloading if we have been destroyed.
  121. if (this.destroyer_.destroyed()) {
  122. throw new shaka.util.Error(
  123. shaka.util.Error.Severity.CRITICAL,
  124. shaka.util.Error.Category.STORAGE,
  125. shaka.util.Error.Code.OPERATION_ABORTED);
  126. }
  127. // Update initData
  128. if (isInitSegment) {
  129. const segmentBytes = shaka.util.BufferUtils.toUint8(response);
  130. const pssh = new shaka.util.Pssh(segmentBytes);
  131. for (const key in pssh.data) {
  132. const index = Number(key);
  133. const data = pssh.data[index];
  134. const systemId = pssh.systemIds[index];
  135. this.onInitData_(data, systemId);
  136. }
  137. }
  138. // Update all our internal stats.
  139. this.estimator_.close(estimateId, response.byteLength);
  140. this.onProgress_(
  141. this.estimator_.getEstimatedProgress(),
  142. this.estimator_.getTotalDownloaded());
  143. return onDownloaded(response);
  144. });
  145. this.groups_.set(groupId, newPromise);
  146. return newPromise;
  147. }
  148. /**
  149. * Add additional async work to the group work queue.
  150. *
  151. * @param {number} groupId
  152. * The group to add this group to. If the group does not exist, a new
  153. * group will be created.
  154. * @param {function():!Promise} callback
  155. * The callback for the async work. Downloading for this group will be
  156. * blocked until the Promise returned by |callback| resolves.
  157. * @return {!Promise} Resolved when this work is complete.
  158. */
  159. queueWork(groupId, callback) {
  160. this.destroyer_.ensureNotDestroyed();
  161. const group = this.groups_.get(groupId) || Promise.resolve();
  162. const newPromise = group.then(async () => {
  163. await callback();
  164. });
  165. this.groups_.set(groupId, newPromise);
  166. return newPromise;
  167. }
  168. /**
  169. * Get a promise that will resolve when all currently queued downloads have
  170. * finished.
  171. *
  172. * @return {!Promise.<number>}
  173. */
  174. async waitToFinish() {
  175. await Promise.all(this.groups_.values());
  176. return this.estimator_.getTotalDownloaded();
  177. }
  178. /**
  179. * Download a segment and return the data in the response.
  180. *
  181. * @param {shaka.extern.Request} request
  182. * @return {!Promise.<BufferSource>}
  183. * @private
  184. */
  185. async fetchSegment_(request) {
  186. const type = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  187. /** @type {!shaka.net.NetworkingEngine.PendingRequest} */
  188. const action = this.networkingEngine_.request(type, request);
  189. const abortCallback = () => {
  190. return action.abort();
  191. };
  192. this.abortCallbacks_.push(abortCallback);
  193. const response = await action.promise;
  194. shaka.util.ArrayUtils.remove(this.abortCallbacks_, abortCallback);
  195. return response.data;
  196. }
  197. };