Source: lib/net/networking_engine.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.net.NetworkingEngine');
  18. goog.require('goog.Uri');
  19. goog.require('goog.asserts');
  20. goog.require('shaka.net.Backoff');
  21. goog.require('shaka.util.ConfigUtils');
  22. goog.require('shaka.util.Error');
  23. goog.require('shaka.util.Functional');
  24. goog.require('shaka.util.IDestroyable');
  25. /**
  26. * NetworkingEngine wraps all networking operations. This accepts plugins that
  27. * handle the actual request. A plugin is registered using registerScheme.
  28. * Each scheme has at most one plugin to handle the request.
  29. *
  30. * @param {function(number, number)=} opt_onSegmentDownloaded Called
  31. * when a segment is downloaded. Passed the duration, in milliseconds, that
  32. * the request took; and the total number of bytes transferred.
  33. *
  34. * @struct
  35. * @constructor
  36. * @implements {shaka.util.IDestroyable}
  37. * @export
  38. */
  39. shaka.net.NetworkingEngine = function(opt_onSegmentDownloaded) {
  40. /** @private {boolean} */
  41. this.destroyed_ = false;
  42. /** @private {!Array.<!Promise>} */
  43. this.requests_ = [];
  44. /** @private {!Array.<shakaExtern.RequestFilter>} */
  45. this.requestFilters_ = [];
  46. /** @private {!Array.<shakaExtern.ResponseFilter>} */
  47. this.responseFilters_ = [];
  48. /** @private {?function(number, number)} */
  49. this.onSegmentDownloaded_ = opt_onSegmentDownloaded || null;
  50. };
  51. /**
  52. * Request types. Allows a filter to decide which requests to read/alter.
  53. *
  54. * @enum {number}
  55. * @export
  56. */
  57. shaka.net.NetworkingEngine.RequestType = {
  58. 'MANIFEST': 0,
  59. 'SEGMENT': 1,
  60. 'LICENSE': 2,
  61. 'APP': 3
  62. };
  63. /**
  64. * Contains the scheme plugins.
  65. *
  66. * @private {!Object.<string, ?shakaExtern.SchemePlugin>}
  67. */
  68. shaka.net.NetworkingEngine.schemes_ = {};
  69. /**
  70. * Registers a scheme plugin. This plugin will handle all requests with the
  71. * given scheme. If a plugin with the same scheme already exists, it is
  72. * replaced.
  73. *
  74. * @param {string} scheme
  75. * @param {shakaExtern.SchemePlugin} plugin
  76. * @export
  77. */
  78. shaka.net.NetworkingEngine.registerScheme = function(scheme, plugin) {
  79. shaka.net.NetworkingEngine.schemes_[scheme] = plugin;
  80. };
  81. /**
  82. * Removes a scheme plugin.
  83. *
  84. * @param {string} scheme
  85. * @export
  86. */
  87. shaka.net.NetworkingEngine.unregisterScheme = function(scheme) {
  88. delete shaka.net.NetworkingEngine.schemes_[scheme];
  89. };
  90. /**
  91. * Registers a new request filter. All filters are applied in the order they
  92. * are registered.
  93. *
  94. * @param {shakaExtern.RequestFilter} filter
  95. * @export
  96. */
  97. shaka.net.NetworkingEngine.prototype.registerRequestFilter = function(filter) {
  98. this.requestFilters_.push(filter);
  99. };
  100. /**
  101. * Removes a request filter.
  102. *
  103. * @param {shakaExtern.RequestFilter} filter
  104. * @export
  105. */
  106. shaka.net.NetworkingEngine.prototype.unregisterRequestFilter =
  107. function(filter) {
  108. var filters = this.requestFilters_;
  109. var i = filters.indexOf(filter);
  110. if (i >= 0) {
  111. filters.splice(i, 1);
  112. }
  113. };
  114. /**
  115. * Clear all request filters.
  116. *
  117. * @export
  118. */
  119. shaka.net.NetworkingEngine.prototype.clearAllRequestFilters = function() {
  120. this.requestFilters_ = [];
  121. };
  122. /**
  123. * Registers a new response filter. All filters are applied in the order they
  124. * are registered.
  125. *
  126. * @param {shakaExtern.ResponseFilter} filter
  127. * @export
  128. */
  129. shaka.net.NetworkingEngine.prototype.registerResponseFilter = function(filter) {
  130. this.responseFilters_.push(filter);
  131. };
  132. /**
  133. * Removes a response filter.
  134. *
  135. * @param {shakaExtern.ResponseFilter} filter
  136. * @export
  137. */
  138. shaka.net.NetworkingEngine.prototype.unregisterResponseFilter =
  139. function(filter) {
  140. var filters = this.responseFilters_;
  141. var i = filters.indexOf(filter);
  142. if (i >= 0) {
  143. filters.splice(i, 1);
  144. }
  145. };
  146. /**
  147. * Clear all response filters.
  148. *
  149. * @export
  150. */
  151. shaka.net.NetworkingEngine.prototype.clearAllResponseFilters = function() {
  152. this.responseFilters_ = [];
  153. };
  154. /**
  155. * Gets a copy of the default retry parameters.
  156. *
  157. * @return {shakaExtern.RetryParameters}
  158. *
  159. * NOTE: The implementation moved to shaka.net.Backoff to avoid a circular
  160. * dependency between the two classes.
  161. */
  162. shaka.net.NetworkingEngine.defaultRetryParameters =
  163. shaka.net.Backoff.defaultRetryParameters;
  164. /**
  165. * Makes a simple network request for the given URIs.
  166. *
  167. * @param {!Array.<string>} uris
  168. * @param {shakaExtern.RetryParameters} retryParams
  169. * @return {shakaExtern.Request}
  170. */
  171. shaka.net.NetworkingEngine.makeRequest = function(
  172. uris, retryParams) {
  173. return {
  174. uris: uris,
  175. method: 'GET',
  176. body: null,
  177. headers: {},
  178. allowCrossSiteCredentials: false,
  179. retryParameters: retryParams
  180. };
  181. };
  182. /**
  183. * @override
  184. * @export
  185. */
  186. shaka.net.NetworkingEngine.prototype.destroy = function() {
  187. var Functional = shaka.util.Functional;
  188. this.destroyed_ = true;
  189. this.requestFilters_ = [];
  190. this.responseFilters_ = [];
  191. var cleanup = [];
  192. for (var i = 0; i < this.requests_.length; ++i) {
  193. cleanup.push(this.requests_[i].catch(Functional.noop));
  194. }
  195. return Promise.all(cleanup);
  196. };
  197. /**
  198. * Makes a network request and returns the resulting data.
  199. *
  200. * @param {shaka.net.NetworkingEngine.RequestType} type
  201. * @param {shakaExtern.Request} request
  202. * @return {!Promise.<shakaExtern.Response>}
  203. * @export
  204. */
  205. shaka.net.NetworkingEngine.prototype.request = function(type, request) {
  206. var cloneObject = shaka.util.ConfigUtils.cloneObject;
  207. // New requests made after destroy is called are rejected.
  208. if (this.destroyed_)
  209. return Promise.reject();
  210. goog.asserts.assert(request.uris && request.uris.length,
  211. 'Request without URIs!');
  212. // If a request comes from outside the library, some parameters may be left
  213. // undefined. To make it easier for application developers, we will fill them
  214. // in with defaults if necessary.
  215. //
  216. // We clone retryParameters and uris so that if a filter modifies the request,
  217. // then it doesn't contaminate future requests.
  218. request.method = request.method || 'GET';
  219. request.headers = request.headers || {};
  220. request.retryParameters = request.retryParameters ?
  221. cloneObject(request.retryParameters) :
  222. shaka.net.NetworkingEngine.defaultRetryParameters();
  223. request.uris = cloneObject(request.uris);
  224. var filterStartMs = Date.now();
  225. // Send to the filter first, in-case they change the URI.
  226. var p = Promise.resolve();
  227. this.requestFilters_.forEach(function(requestFilter) {
  228. // Request filters are resolved sequentially.
  229. p = p.then(requestFilter.bind(null, type, request));
  230. });
  231. // Catch any errors thrown by request filters, and substitute
  232. // them with a Shaka-native error.
  233. p = p.catch(function(e) {
  234. throw new shaka.util.Error(
  235. shaka.util.Error.Severity.CRITICAL,
  236. shaka.util.Error.Category.NETWORK,
  237. shaka.util.Error.Code.REQUEST_FILTER_ERROR, e);
  238. });
  239. // Send out the request, and get a response.
  240. // The entire code is inside a then clause; thus, if a filter
  241. // rejects or errors, the networking engine will never send.
  242. p = p.then(function() {
  243. var filterTimeMs = (Date.now() - filterStartMs);
  244. var backoff = new shaka.net.Backoff(request.retryParameters);
  245. var index = 0;
  246. // Every call to send_ must have an associated attempt() so that the
  247. // accounting in backoff is correct.
  248. return backoff.attempt().then(function() {
  249. return this.send_(type, request, backoff, index, filterTimeMs);
  250. }.bind(this));
  251. }.bind(this));
  252. // Add the request to the array.
  253. this.requests_.push(p);
  254. return p.then(function(response) {
  255. if (this.requests_.indexOf(p) >= 0) {
  256. this.requests_.splice(this.requests_.indexOf(p), 1);
  257. }
  258. if (this.onSegmentDownloaded_ && !response.fromCache &&
  259. type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
  260. this.onSegmentDownloaded_(response.timeMs, response.data.byteLength);
  261. }
  262. return response;
  263. }.bind(this)).catch(function(e) {
  264. // Ignore if using |Promise.reject()| to signal destroy.
  265. if (e) {
  266. goog.asserts.assert(e instanceof shaka.util.Error, 'Wrong error type');
  267. e.severity = shaka.util.Error.Severity.CRITICAL;
  268. }
  269. if (this.requests_.indexOf(p) >= 0) {
  270. this.requests_.splice(this.requests_.indexOf(p), 1);
  271. }
  272. return Promise.reject(e);
  273. }.bind(this));
  274. };
  275. /**
  276. * Sends the given request to the correct plugin and retry using Backoff.
  277. *
  278. * @param {shaka.net.NetworkingEngine.RequestType} type
  279. * @param {shakaExtern.Request} request
  280. * @param {!shaka.net.Backoff} backoff
  281. * @param {number} index
  282. * @param {number} requestFilterTime
  283. * @return {!Promise.<shakaExtern.Response>}
  284. * @private
  285. */
  286. shaka.net.NetworkingEngine.prototype.send_ = function(
  287. type, request, backoff, index, requestFilterTime) {
  288. // Retries sent after destroy is called are rejected.
  289. if (this.destroyed_)
  290. return Promise.reject();
  291. var uri = new goog.Uri(request.uris[index]);
  292. var scheme = uri.getScheme();
  293. if (!scheme) {
  294. // If there is no scheme, infer one from the location.
  295. scheme = shaka.net.NetworkingEngine.getLocationProtocol_();
  296. goog.asserts.assert(scheme[scheme.length - 1] == ':',
  297. 'location.protocol expected to end with a colon!');
  298. // Drop the colon.
  299. scheme = scheme.slice(0, -1);
  300. // Override the original URI to make the scheme explicit.
  301. uri.setScheme(scheme);
  302. request.uris[index] = uri.toString();
  303. }
  304. var plugin = shaka.net.NetworkingEngine.schemes_[scheme];
  305. if (!plugin) {
  306. return Promise.reject(new shaka.util.Error(
  307. shaka.util.Error.Severity.CRITICAL,
  308. shaka.util.Error.Category.NETWORK,
  309. shaka.util.Error.Code.UNSUPPORTED_SCHEME,
  310. uri));
  311. }
  312. var startTimeMs = Date.now();
  313. return plugin(request.uris[index], request, type).then(function(response) {
  314. if (response.timeMs == undefined)
  315. response.timeMs = Date.now() - startTimeMs;
  316. var filterStartMs = Date.now();
  317. var p = Promise.resolve();
  318. this.responseFilters_.forEach(function(responseFilter) {
  319. // Response filters are resolved sequentially.
  320. p = p.then(function() {
  321. return Promise.resolve(responseFilter(type, response));
  322. }.bind(this));
  323. });
  324. // Catch any errors thrown by response filters, and substitute
  325. // them with a Shaka-native error.
  326. p = p.catch(function(e) {
  327. var severity = shaka.util.Error.Severity.CRITICAL;
  328. if (e instanceof shaka.util.Error)
  329. severity = e.severity;
  330. throw new shaka.util.Error(
  331. severity,
  332. shaka.util.Error.Category.NETWORK,
  333. shaka.util.Error.Code.RESPONSE_FILTER_ERROR, e);
  334. });
  335. return p.then(function() {
  336. response.timeMs += Date.now() - filterStartMs;
  337. response.timeMs += requestFilterTime;
  338. return response;
  339. });
  340. }.bind(this)).catch(function(error) {
  341. if (error && error.severity == shaka.util.Error.Severity.RECOVERABLE) {
  342. // Move to the next URI.
  343. index = (index + 1) % request.uris.length;
  344. return backoff.attempt().then(function() {
  345. // Delay has passed. Try again.
  346. return this.send_(type, request, backoff, index, requestFilterTime);
  347. }.bind(this), function() {
  348. // No more attempts are allowed. Fail with the most recent error.
  349. throw error;
  350. });
  351. }
  352. // The error was not recoverable, so do not try again.
  353. // Rethrow the error so the Promise chain stays rejected.
  354. throw error;
  355. }.bind(this));
  356. };
  357. /**
  358. * This is here only for testability. We can't mock location in our tests on
  359. * all browsers, so instead we mock this.
  360. *
  361. * @return {string} The value of location.protocol.
  362. * @private
  363. */
  364. shaka.net.NetworkingEngine.getLocationProtocol_ = function() {
  365. return location.protocol;
  366. };