Source: lib/offline/download_manager.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.offline.DownloadManager');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.net.NetworkingEngine');
  20. goog.require('shaka.offline.OfflineUtils');
  21. goog.require('shaka.util.Error');
  22. goog.require('shaka.util.IDestroyable');
  23. goog.require('shaka.util.MapUtils');
  24. /**
  25. * This manages downloading segments and notifying the app of progress.
  26. *
  27. * @param {shaka.offline.IStorageEngine} storageEngine
  28. * @param {!shaka.net.NetworkingEngine} netEngine
  29. * @param {shakaExtern.RetryParameters} retryParams
  30. * @param {shakaExtern.OfflineConfiguration} config
  31. *
  32. * @struct
  33. * @constructor
  34. * @implements {shaka.util.IDestroyable}
  35. */
  36. shaka.offline.DownloadManager = function(
  37. storageEngine, netEngine, retryParams, config) {
  38. /**
  39. * @private {!Object.<
  40. * string, !Array.<shaka.offline.DownloadManager.Segment>>}
  41. */
  42. this.segments_ = {};
  43. /**
  44. * The IDs of the segments that have been stored for an in-progress
  45. * download(). This is used to cleanup in destroy().
  46. * @private {!Array.<number>}
  47. */
  48. this.storedSegments_ = [];
  49. /** @private {?shakaExtern.OfflineConfiguration} */
  50. this.config_ = config;
  51. /** @private {shaka.offline.IStorageEngine} */
  52. this.storageEngine_ = storageEngine;
  53. /** @private {shaka.net.NetworkingEngine} */
  54. this.netEngine_ = netEngine;
  55. /** @private {?shakaExtern.RetryParameters} */
  56. this.retryParams_ = retryParams;
  57. /** @private {?shakaExtern.ManifestDB} */
  58. this.manifest_ = null;
  59. /** @private {Promise} */
  60. this.promise_ = null;
  61. /**
  62. * The total number of bytes for segments that include a byte range.
  63. * @private {number}
  64. */
  65. this.givenBytesTotal_ = 0;
  66. /**
  67. * The number of bytes downloaded for segments that include a byte range.
  68. * @private {number}
  69. */
  70. this.givenBytesDownloaded_ = 0;
  71. /**
  72. * The total number of bytes estimated based on bandwidth for segments that
  73. * do not include a byte range.
  74. * @private {number}
  75. */
  76. this.bandwidthBytesTotal_ = 0;
  77. /**
  78. * The estimated number of bytes downloaded for segments that do not have
  79. * a byte range.
  80. * @private {number}
  81. */
  82. this.bandwidthBytesDownloaded_ = 0;
  83. };
  84. /**
  85. * @typedef {{
  86. * uris: !Array.<string>,
  87. * startByte: number,
  88. * endByte: ?number,
  89. * bandwidthSize: number,
  90. * segmentDb: shakaExtern.SegmentDataDB
  91. * }}
  92. *
  93. * @property {!Array.<string>} uris
  94. * The URIs to download the segment.
  95. * @property {number} startByte
  96. * The byte index the segment starts at.
  97. * @property {?number} endByte
  98. * The byte index the segment ends at, if present.
  99. * @property {number} bandwidthSize
  100. * The size of the segment as estimated by the bandwidth and segment duration.
  101. * @property {shakaExtern.SegmentDataDB} segmentDb
  102. * The data to store in the database.
  103. */
  104. shaka.offline.DownloadManager.Segment;
  105. /** @override */
  106. shaka.offline.DownloadManager.prototype.destroy = function() {
  107. var storage = this.storageEngine_;
  108. var segments = this.storedSegments_;
  109. var p = this.promise_ || Promise.resolve();
  110. // Don't try to remove segments if there are none. That may trigger an error
  111. // in storage if the DB connection was never created.
  112. if (segments.length) {
  113. p = p.then(function() { return storage.removeKeys('segment', segments); });
  114. }
  115. // Don't destroy() storageEngine since it is owned by Storage.
  116. this.segments_ = {};
  117. this.storedSegments_ = [];
  118. this.config_ = null;
  119. this.storageEngine_ = null;
  120. this.netEngine_ = null;
  121. this.retryParams_ = null;
  122. this.manifest_ = null;
  123. this.promise_ = null;
  124. return p;
  125. };
  126. /**
  127. * Adds a segment to the list to be downloaded.
  128. *
  129. * @param {string} type
  130. * @param {!shaka.media.SegmentReference|!shaka.media.InitSegmentReference} ref
  131. * @param {number} bandwidthSize
  132. * @param {shakaExtern.SegmentDataDB} segmentDb
  133. * The data to store in the database with the data. The |data| field of this
  134. * object will contain the downloaded data.
  135. */
  136. shaka.offline.DownloadManager.prototype.addSegment = function(
  137. type, ref, bandwidthSize, segmentDb) {
  138. this.segments_[type] = this.segments_[type] || [];
  139. this.segments_[type].push({
  140. uris: ref.getUris(),
  141. startByte: ref.startByte,
  142. endByte: ref.endByte,
  143. bandwidthSize: bandwidthSize,
  144. segmentDb: segmentDb
  145. });
  146. };
  147. /**
  148. * Downloads all the segments, stores them in the database, and stores the given
  149. * manifest object.
  150. *
  151. * @param {shakaExtern.ManifestDB} manifest
  152. * @return {!Promise}
  153. */
  154. shaka.offline.DownloadManager.prototype.downloadAndStore = function(manifest) {
  155. var MapUtils = shaka.util.MapUtils;
  156. // Calculate progress estimates.
  157. this.givenBytesTotal_ = 0;
  158. this.givenBytesDownloaded_ = 0;
  159. this.bandwidthBytesTotal_ = 0;
  160. this.bandwidthBytesDownloaded_ = 0;
  161. MapUtils.values(this.segments_).forEach(function(segments) {
  162. segments.forEach(function(segment) {
  163. if (segment.endByte != null)
  164. this.givenBytesTotal_ += (segment.endByte - segment.startByte + 1);
  165. else
  166. this.bandwidthBytesTotal_ += segment.bandwidthSize;
  167. }.bind(this));
  168. }.bind(this));
  169. this.manifest_ = manifest;
  170. // Will be updated as we download for segments without a byte-range.
  171. this.manifest_.size = this.givenBytesTotal_;
  172. // Create separate download chains for different content types. This will
  173. // allow audio and video to be downloaded in parallel.
  174. var async = MapUtils.values(this.segments_).map(function(segments) {
  175. var i = 0;
  176. var downloadNext = (function() {
  177. if (!this.config_) {
  178. return Promise.reject(new shaka.util.Error(
  179. shaka.util.Error.Severity.CRITICAL,
  180. shaka.util.Error.Category.STORAGE,
  181. shaka.util.Error.Code.OPERATION_ABORTED));
  182. }
  183. if (i >= segments.length) return Promise.resolve();
  184. var segment = segments[i++];
  185. return this.downloadSegment_(segment).then(downloadNext);
  186. }.bind(this));
  187. return downloadNext();
  188. }.bind(this));
  189. this.segments_ = {};
  190. this.promise_ = Promise.all(async).then(function() {
  191. return this.storageEngine_.insert('manifest', manifest);
  192. }.bind(this)).then(function() {
  193. this.storedSegments_ = [];
  194. }.bind(this));
  195. return this.promise_;
  196. };
  197. /**
  198. * Downloads the given segment and calls the callback.
  199. *
  200. * @param {shaka.offline.DownloadManager.Segment} segment
  201. * @return {!Promise}
  202. * @private
  203. */
  204. shaka.offline.DownloadManager.prototype.downloadSegment_ = function(segment) {
  205. goog.asserts.assert(this.retryParams_,
  206. 'DownloadManager must not be destroyed');
  207. var type = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  208. var request =
  209. shaka.net.NetworkingEngine.makeRequest(segment.uris, this.retryParams_);
  210. if (segment.startByte != 0 || segment.endByte != null) {
  211. var end = segment.endByte == null ? '' : segment.endByte;
  212. request.headers['Range'] = 'bytes=' + segment.startByte + '-' + end;
  213. }
  214. var byteCount;
  215. return this.netEngine_.request(type, request)
  216. .then(function(response) {
  217. if (!this.manifest_) {
  218. return Promise.reject(new shaka.util.Error(
  219. shaka.util.Error.Severity.CRITICAL,
  220. shaka.util.Error.Category.STORAGE,
  221. shaka.util.Error.Code.OPERATION_ABORTED));
  222. }
  223. byteCount = response.data.byteLength;
  224. this.storedSegments_.push(segment.segmentDb.key);
  225. segment.segmentDb.data = response.data;
  226. return this.storageEngine_.insert('segment', segment.segmentDb);
  227. }.bind(this))
  228. .then(function() {
  229. if (!this.manifest_) {
  230. return Promise.reject(new shaka.util.Error(
  231. shaka.util.Error.Severity.CRITICAL,
  232. shaka.util.Error.Category.STORAGE,
  233. shaka.util.Error.Code.OPERATION_ABORTED));
  234. }
  235. if (segment.endByte == null) {
  236. // We didn't know the size, so it was an estimate.
  237. this.manifest_.size += byteCount;
  238. this.bandwidthBytesDownloaded_ += segment.bandwidthSize;
  239. } else {
  240. goog.asserts.assert(
  241. byteCount == (segment.endByte - segment.startByte + 1),
  242. 'Incorrect download size');
  243. this.givenBytesDownloaded_ += byteCount;
  244. }
  245. this.updateProgress_();
  246. }.bind(this));
  247. };
  248. /**
  249. * Calls the progress callback.
  250. * @private
  251. */
  252. shaka.offline.DownloadManager.prototype.updateProgress_ = function() {
  253. var progress = (this.givenBytesDownloaded_ + this.bandwidthBytesDownloaded_) /
  254. (this.givenBytesTotal_ + this.bandwidthBytesTotal_);
  255. goog.asserts.assert(this.manifest_, 'Must not be destroyed');
  256. var manifest = shaka.offline.OfflineUtils.getStoredContent(this.manifest_);
  257. this.config_.progressCallback(manifest, progress);
  258. };