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.log');
  21. goog.require('shaka.net.Backoff');
  22. goog.require('shaka.util.AbortableOperation');
  23. goog.require('shaka.util.ArrayUtils');
  24. goog.require('shaka.util.ConfigUtils');
  25. goog.require('shaka.util.Error');
  26. goog.require('shaka.util.IDestroyable');
  27. goog.require('shaka.util.OperationManager');
  28. /**
  29. * NetworkingEngine wraps all networking operations. This accepts plugins that
  30. * handle the actual request. A plugin is registered using registerScheme.
  31. * Each scheme has at most one plugin to handle the request.
  32. *
  33. * @param {function(number, number)=} opt_onSegmentDownloaded Called
  34. * when a segment is downloaded. Passed the duration, in milliseconds, that
  35. * the request took, and the total number of bytes transferred.
  36. *
  37. * @struct
  38. * @constructor
  39. * @implements {shaka.util.IDestroyable}
  40. * @export
  41. */
  42. shaka.net.NetworkingEngine = function(opt_onSegmentDownloaded) {
  43. /** @private {boolean} */
  44. this.destroyed_ = false;
  45. /** @private {!shaka.util.OperationManager} */
  46. this.operationManager_ = new shaka.util.OperationManager();
  47. /** @private {!Array.<shakaExtern.RequestFilter>} */
  48. this.requestFilters_ = [];
  49. /** @private {!Array.<shakaExtern.ResponseFilter>} */
  50. this.responseFilters_ = [];
  51. /** @private {?function(number, number)} */
  52. this.onSegmentDownloaded_ = opt_onSegmentDownloaded || null;
  53. };
  54. /**
  55. * Request types. Allows a filter to decide which requests to read/alter.
  56. *
  57. * @enum {number}
  58. * @export
  59. */
  60. shaka.net.NetworkingEngine.RequestType = {
  61. 'MANIFEST': 0,
  62. 'SEGMENT': 1,
  63. 'LICENSE': 2,
  64. 'APP': 3
  65. };
  66. /**
  67. * Priority level for network scheme plugins.
  68. * If multiple plugins are provided for the same scheme, only the
  69. * highest-priority one is used.
  70. *
  71. * @enum {number}
  72. * @export
  73. */
  74. shaka.net.NetworkingEngine.PluginPriority = {
  75. 'FALLBACK': 1,
  76. 'PREFERRED': 2,
  77. 'APPLICATION': 3
  78. };
  79. /**
  80. * @typedef {{
  81. * plugin: shakaExtern.SchemePlugin,
  82. * priority: number
  83. * }}
  84. * @property {shakaExtern.SchemePlugin} plugin
  85. * The associated plugin.
  86. * @property {number} priority
  87. * The plugin's priority.
  88. */
  89. shaka.net.NetworkingEngine.SchemeObject;
  90. /**
  91. * Contains the scheme plugins.
  92. *
  93. * @private {!Object.<string, shaka.net.NetworkingEngine.SchemeObject>}
  94. */
  95. shaka.net.NetworkingEngine.schemes_ = {};
  96. /**
  97. * Registers a scheme plugin. This plugin will handle all requests with the
  98. * given scheme. If a plugin with the same scheme already exists, it is
  99. * replaced, unless the existing plugin is of higher priority.
  100. * If no priority is provided, this defaults to the highest priority of
  101. * APPLICATION.
  102. *
  103. * @param {string} scheme
  104. * @param {shakaExtern.SchemePlugin} plugin
  105. * @param {number=} opt_priority
  106. * @export
  107. */
  108. shaka.net.NetworkingEngine.registerScheme =
  109. function(scheme, plugin, opt_priority) {
  110. goog.asserts.assert(opt_priority == undefined || opt_priority > 0,
  111. 'explicit priority must be > 0');
  112. let priority =
  113. opt_priority || shaka.net.NetworkingEngine.PluginPriority.APPLICATION;
  114. let existing = shaka.net.NetworkingEngine.schemes_[scheme];
  115. if (!existing || priority >= existing.priority) {
  116. shaka.net.NetworkingEngine.schemes_[scheme] = {
  117. priority: priority,
  118. plugin: plugin
  119. };
  120. }
  121. };
  122. /**
  123. * Removes a scheme plugin.
  124. *
  125. * @param {string} scheme
  126. * @export
  127. */
  128. shaka.net.NetworkingEngine.unregisterScheme = function(scheme) {
  129. delete shaka.net.NetworkingEngine.schemes_[scheme];
  130. };
  131. /**
  132. * Registers a new request filter. All filters are applied in the order they
  133. * are registered.
  134. *
  135. * @param {shakaExtern.RequestFilter} filter
  136. * @export
  137. */
  138. shaka.net.NetworkingEngine.prototype.registerRequestFilter = function(filter) {
  139. this.requestFilters_.push(filter);
  140. };
  141. /**
  142. * Removes a request filter.
  143. *
  144. * @param {shakaExtern.RequestFilter} filter
  145. * @export
  146. */
  147. shaka.net.NetworkingEngine.prototype.unregisterRequestFilter =
  148. function(filter) {
  149. shaka.util.ArrayUtils.remove(this.requestFilters_, filter);
  150. };
  151. /**
  152. * Clears all request filters.
  153. *
  154. * @export
  155. */
  156. shaka.net.NetworkingEngine.prototype.clearAllRequestFilters = function() {
  157. this.requestFilters_ = [];
  158. };
  159. /**
  160. * Registers a new response filter. All filters are applied in the order they
  161. * are registered.
  162. *
  163. * @param {shakaExtern.ResponseFilter} filter
  164. * @export
  165. */
  166. shaka.net.NetworkingEngine.prototype.registerResponseFilter = function(filter) {
  167. this.responseFilters_.push(filter);
  168. };
  169. /**
  170. * Removes a response filter.
  171. *
  172. * @param {shakaExtern.ResponseFilter} filter
  173. * @export
  174. */
  175. shaka.net.NetworkingEngine.prototype.unregisterResponseFilter =
  176. function(filter) {
  177. shaka.util.ArrayUtils.remove(this.responseFilters_, filter);
  178. };
  179. /**
  180. * Clears all response filters.
  181. *
  182. * @export
  183. */
  184. shaka.net.NetworkingEngine.prototype.clearAllResponseFilters = function() {
  185. this.responseFilters_ = [];
  186. };
  187. /**
  188. * Gets a copy of the default retry parameters.
  189. *
  190. * @return {shakaExtern.RetryParameters}
  191. *
  192. * NOTE: The implementation moved to shaka.net.Backoff to avoid a circular
  193. * dependency between the two classes.
  194. */
  195. shaka.net.NetworkingEngine.defaultRetryParameters =
  196. shaka.net.Backoff.defaultRetryParameters;
  197. /**
  198. * Makes a simple network request for the given URIs.
  199. *
  200. * @param {!Array.<string>} uris
  201. * @param {shakaExtern.RetryParameters} retryParams
  202. * @return {shakaExtern.Request}
  203. */
  204. shaka.net.NetworkingEngine.makeRequest = function(
  205. uris, retryParams) {
  206. return {
  207. uris: uris,
  208. method: 'GET',
  209. body: null,
  210. headers: {},
  211. allowCrossSiteCredentials: false,
  212. retryParameters: retryParams
  213. };
  214. };
  215. /**
  216. * @override
  217. * @export
  218. */
  219. shaka.net.NetworkingEngine.prototype.destroy = function() {
  220. this.destroyed_ = true;
  221. this.requestFilters_ = [];
  222. this.responseFilters_ = [];
  223. return this.operationManager_.destroy();
  224. };
  225. /**
  226. * Shims return values from requests to look like Promises, so that callers have
  227. * time to update to the new operation-based API.
  228. *
  229. * @param {!shakaExtern.IAbortableOperation.<shakaExtern.Response>} operation
  230. * @return {!shakaExtern.IAbortableOperation.<shakaExtern.Response>}
  231. * @private
  232. */
  233. shaka.net.NetworkingEngine.shimRequests_ = function(operation) {
  234. // TODO: remove in v2.5
  235. operation.then = (onSuccess, onError) => {
  236. shaka.log.alwaysWarn('The network request interface has changed! Please ' +
  237. 'update your application to the new interface, ' +
  238. 'which allows operations to be aborted. Support ' +
  239. 'for the old API will be removed in v2.5.');
  240. return operation.promise.then(onSuccess, onError);
  241. };
  242. operation.catch = (onError) => {
  243. shaka.log.alwaysWarn('The network request interface has changed! Please ' +
  244. 'update your application to the new interface, ' +
  245. 'which allows operations to be aborted. Support ' +
  246. 'for the old API will be removed in v2.5.');
  247. return operation.promise.catch(onError);
  248. };
  249. return operation;
  250. };
  251. /**
  252. * Makes a network request and returns the resulting data.
  253. *
  254. * @param {shaka.net.NetworkingEngine.RequestType} type
  255. * @param {shakaExtern.Request} request
  256. * @return {!shakaExtern.IAbortableOperation.<shakaExtern.Response>}
  257. * @export
  258. */
  259. shaka.net.NetworkingEngine.prototype.request = function(type, request) {
  260. let cloneObject = shaka.util.ConfigUtils.cloneObject;
  261. // Reject all requests made after destroy is called.
  262. if (this.destroyed_) {
  263. return shaka.net.NetworkingEngine.shimRequests_(
  264. shaka.util.AbortableOperation.aborted());
  265. }
  266. goog.asserts.assert(request.uris && request.uris.length,
  267. 'Request without URIs!');
  268. // If a request comes from outside the library, some parameters may be left
  269. // undefined. To make it easier for application developers, we will fill them
  270. // in with defaults if necessary.
  271. //
  272. // We clone retryParameters and uris so that if a filter modifies the request,
  273. // it doesn't contaminate future requests.
  274. request.method = request.method || 'GET';
  275. request.headers = request.headers || {};
  276. request.retryParameters = request.retryParameters ?
  277. cloneObject(request.retryParameters) :
  278. shaka.net.NetworkingEngine.defaultRetryParameters();
  279. request.uris = cloneObject(request.uris);
  280. let requestFilterOperation = this.filterRequest_(type, request);
  281. let requestOperation = requestFilterOperation.chain(
  282. () => this.makeRequestWithRetry_(type, request));
  283. let responseFilterOperation = requestOperation.chain(
  284. (response) => this.filterResponse_(type, response));
  285. // Keep track of time spent in filters.
  286. let requestFilterStartTime = Date.now();
  287. let requestFilterMs = 0;
  288. requestFilterOperation.promise.then(() => {
  289. requestFilterMs = Date.now() - requestFilterStartTime;
  290. }, () => {}); // Silence errors in this fork of the Promise chain.
  291. let responseFilterStartTime = 0;
  292. requestOperation.promise.then(() => {
  293. responseFilterStartTime = Date.now();
  294. }, () => {}); // Silence errors in this fork of the Promise chain.
  295. let operation = responseFilterOperation.chain((response) => {
  296. let responseFilterMs = Date.now() - responseFilterStartTime;
  297. response.timeMs += requestFilterMs;
  298. response.timeMs += responseFilterMs;
  299. if (this.onSegmentDownloaded_ && !response.fromCache &&
  300. type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
  301. this.onSegmentDownloaded_(response.timeMs, response.data.byteLength);
  302. }
  303. return response;
  304. }, (e) => {
  305. // Any error thrown from elsewhere should be recategorized as CRITICAL here.
  306. // This is because by the time it gets here, we've exhausted retries.
  307. if (e) {
  308. goog.asserts.assert(e instanceof shaka.util.Error, 'Wrong error type');
  309. e.severity = shaka.util.Error.Severity.CRITICAL;
  310. }
  311. throw e;
  312. });
  313. // Add the operation to the manager for later cleanup.
  314. this.operationManager_.manage(operation);
  315. return shaka.net.NetworkingEngine.shimRequests_(operation);
  316. };
  317. /**
  318. * @param {shaka.net.NetworkingEngine.RequestType} type
  319. * @param {shakaExtern.Request} request
  320. * @return {!shakaExtern.IAbortableOperation.<undefined>}
  321. * @private
  322. */
  323. shaka.net.NetworkingEngine.prototype.filterRequest_ = function(type, request) {
  324. let filterOperation = shaka.util.AbortableOperation.completed(undefined);
  325. this.requestFilters_.forEach((requestFilter) => {
  326. // Request filters are run sequentially.
  327. filterOperation =
  328. filterOperation.chain(() => requestFilter(type, request));
  329. });
  330. // Catch any errors thrown by request filters, and substitute
  331. // them with a Shaka-native error.
  332. return filterOperation.chain(undefined, (e) => {
  333. if (e && e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  334. // Don't change anything if the operation was aborted.
  335. throw e;
  336. }
  337. throw new shaka.util.Error(
  338. shaka.util.Error.Severity.CRITICAL,
  339. shaka.util.Error.Category.NETWORK,
  340. shaka.util.Error.Code.REQUEST_FILTER_ERROR, e);
  341. });
  342. };
  343. /**
  344. * @param {shaka.net.NetworkingEngine.RequestType} type
  345. * @param {shakaExtern.Request} request
  346. * @return {!shakaExtern.IAbortableOperation.<shakaExtern.Response>}
  347. * @private
  348. */
  349. shaka.net.NetworkingEngine.prototype.makeRequestWithRetry_ =
  350. function(type, request) {
  351. let backoff = new shaka.net.Backoff(
  352. request.retryParameters, /* autoReset */ false);
  353. let index = 0;
  354. return this.send_(type, request, backoff, index, /* lastError */ null);
  355. };
  356. /**
  357. * Sends the given request to the correct plugin and retry using Backoff.
  358. *
  359. * @param {shaka.net.NetworkingEngine.RequestType} type
  360. * @param {shakaExtern.Request} request
  361. * @param {!shaka.net.Backoff} backoff
  362. * @param {number} index
  363. * @param {?shaka.util.Error} lastError
  364. * @return {!shakaExtern.IAbortableOperation.<shakaExtern.Response>}
  365. * @private
  366. */
  367. shaka.net.NetworkingEngine.prototype.send_ = function(
  368. type, request, backoff, index, lastError) {
  369. let uri = new goog.Uri(request.uris[index]);
  370. let scheme = uri.getScheme();
  371. if (!scheme) {
  372. // If there is no scheme, infer one from the location.
  373. scheme = shaka.net.NetworkingEngine.getLocationProtocol_();
  374. goog.asserts.assert(scheme[scheme.length - 1] == ':',
  375. 'location.protocol expected to end with a colon!');
  376. // Drop the colon.
  377. scheme = scheme.slice(0, -1);
  378. // Override the original URI to make the scheme explicit.
  379. uri.setScheme(scheme);
  380. request.uris[index] = uri.toString();
  381. }
  382. let object = shaka.net.NetworkingEngine.schemes_[scheme];
  383. let plugin = object ? object.plugin : null;
  384. if (!plugin) {
  385. return shaka.util.AbortableOperation.failed(
  386. new shaka.util.Error(
  387. shaka.util.Error.Severity.CRITICAL,
  388. shaka.util.Error.Category.NETWORK,
  389. shaka.util.Error.Code.UNSUPPORTED_SCHEME,
  390. uri));
  391. }
  392. // Every attempt must have an associated backoff.attempt() call so that the
  393. // accounting is correct.
  394. let backoffOperation =
  395. shaka.util.AbortableOperation.notAbortable(backoff.attempt());
  396. let startTimeMs;
  397. let sendOperation = backoffOperation.chain(() => {
  398. if (this.destroyed_) {
  399. return shaka.util.AbortableOperation.aborted();
  400. }
  401. startTimeMs = Date.now();
  402. let operation = plugin(request.uris[index], request, type);
  403. // Backward compatibility with older scheme plugins.
  404. // TODO: remove in v2.5
  405. if (operation.promise == undefined) {
  406. shaka.log.alwaysWarn('The scheme plugin interface has changed! Please ' +
  407. 'update your scheme plugins to the new interface ' +
  408. 'to add support for abort(). Support for the old ' +
  409. 'plugin interface will be removed in v2.5.');
  410. // The return was just a promise, so wrap it into an operation.
  411. let schemePromise = /** @type {!Promise} */(operation);
  412. operation = shaka.util.AbortableOperation.notAbortable(schemePromise);
  413. }
  414. return operation;
  415. }).chain((response) => {
  416. if (response.timeMs == undefined) {
  417. response.timeMs = Date.now() - startTimeMs;
  418. }
  419. return response;
  420. }, (error) => {
  421. if (error && error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  422. // Don't change anything if the operation was aborted.
  423. throw error;
  424. }
  425. if (this.destroyed_) {
  426. return shaka.util.AbortableOperation.aborted();
  427. }
  428. if (error && error.severity == shaka.util.Error.Severity.RECOVERABLE) {
  429. // Move to the next URI.
  430. index = (index + 1) % request.uris.length;
  431. let shakaError = /** @type {shaka.util.Error} */(error);
  432. return this.send_(type, request, backoff, index, shakaError);
  433. }
  434. // The error was not recoverable, so do not try again.
  435. // Rethrow the error so the Promise chain stays rejected.
  436. throw error || lastError;
  437. });
  438. return sendOperation;
  439. };
  440. /**
  441. * @param {shaka.net.NetworkingEngine.RequestType} type
  442. * @param {shakaExtern.Response} response
  443. * @return {!shakaExtern.IAbortableOperation.<shakaExtern.Response>}
  444. * @private
  445. */
  446. shaka.net.NetworkingEngine.prototype.filterResponse_ =
  447. function(type, response) {
  448. let filterOperation = shaka.util.AbortableOperation.completed(undefined);
  449. this.responseFilters_.forEach((responseFilter) => {
  450. // Response filters are run sequentially.
  451. filterOperation =
  452. filterOperation.chain(() => responseFilter(type, response));
  453. });
  454. return filterOperation.chain(() => {
  455. // If successful, return the filtered response.
  456. return response;
  457. }, (e) => {
  458. // Catch any errors thrown by request filters, and substitute
  459. // them with a Shaka-native error.
  460. if (e && e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  461. // Don't change anything if the operation was aborted.
  462. throw e;
  463. }
  464. // The error is assumed to be critical if the original wasn't a Shaka error.
  465. let severity = shaka.util.Error.Severity.CRITICAL;
  466. if (e instanceof shaka.util.Error) {
  467. severity = e.severity;
  468. }
  469. throw new shaka.util.Error(
  470. severity,
  471. shaka.util.Error.Category.NETWORK,
  472. shaka.util.Error.Code.RESPONSE_FILTER_ERROR, e);
  473. });
  474. };
  475. /**
  476. * This is here only for testability. We can't mock location in our tests on
  477. * all browsers, so instead we mock this.
  478. *
  479. * @return {string} The value of location.protocol.
  480. * @private
  481. */
  482. shaka.net.NetworkingEngine.getLocationProtocol_ = function() {
  483. return location.protocol;
  484. };