Source: lib/net/networking_engine.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.net.NetworkingEngine');
  7. goog.provide('shaka.net.NetworkingEngine.AdvancedRequestType');
  8. goog.provide('shaka.net.NetworkingEngine.RequestType');
  9. goog.provide('shaka.net.NetworkingEngine.PendingRequest');
  10. goog.require('goog.Uri');
  11. goog.require('goog.asserts');
  12. goog.require('shaka.net.Backoff');
  13. goog.require('shaka.util.AbortableOperation');
  14. goog.require('shaka.util.BufferUtils');
  15. goog.require('shaka.util.Error');
  16. goog.require('shaka.util.FakeEvent');
  17. goog.require('shaka.util.FakeEventTarget');
  18. goog.require('shaka.util.IDestroyable');
  19. goog.require('shaka.util.ObjectUtils');
  20. goog.require('shaka.util.OperationManager');
  21. goog.require('shaka.util.Timer');
  22. /**
  23. * @event shaka.net.NetworkingEngine.RetryEvent
  24. * @description Fired when the networking engine receives a recoverable error
  25. * and retries.
  26. * @property {string} type
  27. * 'retry'
  28. * @property {?shaka.util.Error} error
  29. * The error that caused the retry. If it was a non-Shaka error, this is set
  30. * to null.
  31. * @exportDoc
  32. */
  33. /**
  34. * NetworkingEngine wraps all networking operations. This accepts plugins that
  35. * handle the actual request. A plugin is registered using registerScheme.
  36. * Each scheme has at most one plugin to handle the request.
  37. *
  38. * @implements {shaka.util.IDestroyable}
  39. * @export
  40. */
  41. shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
  42. /**
  43. * @param {shaka.net.NetworkingEngine.onProgressUpdated=} onProgressUpdated
  44. * Called when
  45. * a progress event is triggered. Passed the duration, in milliseconds,
  46. * that the request took, the number of bytes transferred, and the boolean
  47. * of whether the switching is allowed.
  48. * @param {shaka.net.NetworkingEngine.OnHeadersReceived=} onHeadersReceived
  49. * Called when the headers are received for a download.
  50. * @param {shaka.net.NetworkingEngine.OnDownloadCompleted=
  51. * } onDownloadCompleted Called when a download completed successfully.
  52. * @param {shaka.net.NetworkingEngine.OnDownloadFailed=} onDownloadFailed
  53. * Called when a download fails, for any reason.
  54. * @param {shaka.net.NetworkingEngine.OnRequest=} onRequest
  55. * Called when a request is made
  56. * @param {shaka.net.NetworkingEngine.OnRetry=} onRetry
  57. * Called when a request retry is made
  58. * @param {shaka.net.NetworkingEngine.OnResponse=} onResponse
  59. * Called when receive the response
  60. */
  61. constructor(onProgressUpdated, onHeadersReceived, onDownloadCompleted,
  62. onDownloadFailed, onRequest, onRetry, onResponse) {
  63. super();
  64. /** @private {?shaka.extern.NetworkingConfiguration} */
  65. this.config_ = null;
  66. /** @private {boolean} */
  67. this.destroyed_ = false;
  68. /** @private {!shaka.util.OperationManager} */
  69. this.operationManager_ = new shaka.util.OperationManager();
  70. /** @private {!Set<shaka.extern.RequestFilter>} */
  71. this.requestFilters_ = new Set();
  72. /** @private {!Set<shaka.extern.ResponseFilter>} */
  73. this.responseFilters_ = new Set();
  74. /** @private {?shaka.net.NetworkingEngine.onProgressUpdated} */
  75. this.onProgressUpdated_ = onProgressUpdated || null;
  76. /** @private {?shaka.net.NetworkingEngine.OnHeadersReceived} */
  77. this.onHeadersReceived_ = onHeadersReceived || null;
  78. /** @private {?shaka.net.NetworkingEngine.OnDownloadCompleted} */
  79. this.onDownloadCompleted_ = onDownloadCompleted || null;
  80. /** @private {?shaka.net.NetworkingEngine.OnDownloadFailed} */
  81. this.onDownloadFailed_ = onDownloadFailed || null;
  82. /** @private {?shaka.net.NetworkingEngine.OnRequest} */
  83. this.onRequest_ = onRequest || null;
  84. /** @private {?shaka.net.NetworkingEngine.OnRetry} */
  85. this.onRetry_ = onRetry || null;
  86. /** @private {?shaka.net.NetworkingEngine.OnResponse} */
  87. this.onResponse_ = onResponse || null;
  88. /** @private {!Map<string, string>} */
  89. this.hostCommonAccessTokenMap_ = new Map();
  90. }
  91. /**
  92. * @param {shaka.extern.NetworkingConfiguration} config
  93. * @export
  94. */
  95. configure(config) {
  96. this.config_ = config;
  97. }
  98. /**
  99. * Registers a scheme plugin. This plugin will handle all requests with the
  100. * given scheme. If a plugin with the same scheme already exists, it is
  101. * replaced, unless the existing plugin is of higher priority.
  102. * If no priority is provided, this defaults to the highest priority of
  103. * APPLICATION.
  104. *
  105. * @param {string} scheme
  106. * @param {shaka.extern.SchemePlugin} plugin
  107. * @param {number=} priority
  108. * @param {boolean=} progressSupport
  109. * @export
  110. */
  111. static registerScheme(scheme, plugin, priority, progressSupport = false) {
  112. goog.asserts.assert(
  113. priority == undefined || priority > 0, 'explicit priority must be > 0');
  114. priority =
  115. priority || shaka.net.NetworkingEngine.PluginPriority.APPLICATION;
  116. const existing = shaka.net.NetworkingEngine.schemes_.get(scheme);
  117. if (!existing || priority >= existing.priority) {
  118. shaka.net.NetworkingEngine.schemes_.set(scheme, {
  119. priority: priority,
  120. plugin: plugin,
  121. progressSupport: progressSupport,
  122. });
  123. }
  124. }
  125. /**
  126. * Removes a scheme plugin.
  127. *
  128. * @param {string} scheme
  129. * @export
  130. */
  131. static unregisterScheme(scheme) {
  132. shaka.net.NetworkingEngine.schemes_.delete(scheme);
  133. }
  134. /**
  135. * Copies all of the filters from this networking engine into another.
  136. * @param {!shaka.net.NetworkingEngine} other
  137. */
  138. copyFiltersInto(other) {
  139. for (const filter of this.requestFilters_) {
  140. other.requestFilters_.add(filter);
  141. }
  142. for (const filter of this.responseFilters_) {
  143. other.responseFilters_.add(filter);
  144. }
  145. }
  146. /**
  147. * Registers a new request filter. All filters are applied in the order they
  148. * are registered.
  149. *
  150. * @param {shaka.extern.RequestFilter} filter
  151. * @export
  152. */
  153. registerRequestFilter(filter) {
  154. this.requestFilters_.add(filter);
  155. }
  156. /**
  157. * Removes a request filter.
  158. *
  159. * @param {shaka.extern.RequestFilter} filter
  160. * @export
  161. */
  162. unregisterRequestFilter(filter) {
  163. this.requestFilters_.delete(filter);
  164. }
  165. /**
  166. * Clears all request filters.
  167. *
  168. * @export
  169. */
  170. clearAllRequestFilters() {
  171. this.requestFilters_.clear();
  172. }
  173. /**
  174. * Registers a new response filter. All filters are applied in the order they
  175. * are registered.
  176. *
  177. * @param {shaka.extern.ResponseFilter} filter
  178. * @export
  179. */
  180. registerResponseFilter(filter) {
  181. this.responseFilters_.add(filter);
  182. }
  183. /**
  184. * Removes a response filter.
  185. *
  186. * @param {shaka.extern.ResponseFilter} filter
  187. * @export
  188. */
  189. unregisterResponseFilter(filter) {
  190. this.responseFilters_.delete(filter);
  191. }
  192. /**
  193. * Clears all response filters.
  194. *
  195. * @export
  196. */
  197. clearAllResponseFilters() {
  198. this.responseFilters_.clear();
  199. }
  200. /**
  201. * Clears Common Access Token map.
  202. *
  203. * @export
  204. */
  205. clearCommonAccessTokenMap() {
  206. this.hostCommonAccessTokenMap_.clear();
  207. }
  208. /**
  209. * Gets a copy of the default retry parameters.
  210. *
  211. * @return {shaka.extern.RetryParameters}
  212. *
  213. * NOTE: The implementation moved to shaka.net.Backoff to avoid a circular
  214. * dependency between the two classes.
  215. *
  216. * @export
  217. */
  218. static defaultRetryParameters() {
  219. return shaka.net.Backoff.defaultRetryParameters();
  220. }
  221. /**
  222. * Makes a simple network request for the given URIs.
  223. *
  224. * @param {!Array<string>} uris
  225. * @param {shaka.extern.RetryParameters} retryParams
  226. * @param {?function(BufferSource):!Promise=} streamDataCallback
  227. * @return {shaka.extern.Request}
  228. * @export
  229. */
  230. static makeRequest(uris, retryParams, streamDataCallback = null) {
  231. return {
  232. uris: uris,
  233. method: 'GET',
  234. body: null,
  235. headers: {},
  236. allowCrossSiteCredentials: false,
  237. retryParameters: retryParams,
  238. licenseRequestType: null,
  239. sessionId: null,
  240. drmInfo: null,
  241. initData: null,
  242. initDataType: null,
  243. streamDataCallback: streamDataCallback,
  244. };
  245. }
  246. /**
  247. * @override
  248. * @export
  249. */
  250. destroy() {
  251. this.destroyed_ = true;
  252. this.requestFilters_.clear();
  253. this.responseFilters_.clear();
  254. this.hostCommonAccessTokenMap_.clear();
  255. // FakeEventTarget implements IReleasable
  256. super.release();
  257. return this.operationManager_.destroy();
  258. }
  259. /**
  260. * Makes a network request and returns the resulting data.
  261. *
  262. * @param {shaka.net.NetworkingEngine.RequestType} type
  263. * @param {shaka.extern.Request} request
  264. * @param {shaka.extern.RequestContext=} context
  265. * @return {!shaka.net.NetworkingEngine.PendingRequest}
  266. * @export
  267. */
  268. request(type, request, context) {
  269. const ObjectUtils = shaka.util.ObjectUtils;
  270. const numBytesRemainingObj =
  271. new shaka.net.NetworkingEngine.NumBytesRemainingClass();
  272. // Reject all requests made after destroy is called.
  273. if (this.destroyed_) {
  274. const p = Promise.reject(new shaka.util.Error(
  275. shaka.util.Error.Severity.CRITICAL,
  276. shaka.util.Error.Category.PLAYER,
  277. shaka.util.Error.Code.OPERATION_ABORTED));
  278. // Silence uncaught rejection errors, which may otherwise occur any place
  279. // we don't explicitly handle aborted operations.
  280. p.catch(() => {});
  281. return new shaka.net.NetworkingEngine.PendingRequest(
  282. p, () => Promise.resolve(), numBytesRemainingObj);
  283. }
  284. goog.asserts.assert(
  285. request.uris && request.uris.length, 'Request without URIs!');
  286. // If a request comes from outside the library, some parameters may be left
  287. // undefined. To make it easier for application developers, we will fill
  288. // them in with defaults if necessary.
  289. //
  290. // We clone retryParameters and uris so that if a filter modifies the
  291. // request, it doesn't contaminate future requests.
  292. request.method = request.method || 'GET';
  293. request.headers = request.headers || {};
  294. request.retryParameters = request.retryParameters ?
  295. ObjectUtils.cloneObject(request.retryParameters) :
  296. shaka.net.NetworkingEngine.defaultRetryParameters();
  297. request.uris = ObjectUtils.cloneObject(request.uris);
  298. // Apply the registered filters to the request.
  299. const requestFilterOperation = this.filterRequest_(type, request, context);
  300. const requestOperation = requestFilterOperation.chain(
  301. () => this.makeRequestWithRetry_(type, request, context,
  302. numBytesRemainingObj));
  303. const responseFilterOperation = requestOperation.chain(
  304. (responseAndGotProgress) =>
  305. this.filterResponse_(type, responseAndGotProgress, context));
  306. // Keep track of time spent in filters.
  307. const requestFilterStartTime = Date.now();
  308. let requestFilterMs = 0;
  309. requestFilterOperation.promise.then(() => {
  310. requestFilterMs = Date.now() - requestFilterStartTime;
  311. }, () => {}); // Silence errors in this fork of the Promise chain.
  312. let responseFilterStartTime = 0;
  313. requestOperation.promise.then(() => {
  314. responseFilterStartTime = Date.now();
  315. }, () => {}); // Silence errors in this fork of the Promise chain.
  316. const op = responseFilterOperation.chain((responseAndGotProgress) => {
  317. const responseFilterMs = Date.now() - responseFilterStartTime;
  318. const response = responseAndGotProgress.response;
  319. response.timeMs += requestFilterMs;
  320. response.timeMs += responseFilterMs;
  321. if (!responseAndGotProgress.gotProgress &&
  322. this.onProgressUpdated_ &&
  323. !response.fromCache &&
  324. request.method != 'HEAD' &&
  325. type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
  326. const allowSwitch = this.allowSwitch_(context);
  327. this.onProgressUpdated_(
  328. response.timeMs, response.data.byteLength, allowSwitch, request,
  329. context);
  330. }
  331. if (this.onResponse_) {
  332. this.onResponse_(type, response, context);
  333. }
  334. return response;
  335. }, (e) => {
  336. // Any error thrown from elsewhere should be recategorized as CRITICAL
  337. // here. This is because by the time it gets here, we've exhausted
  338. // retries.
  339. if (e) {
  340. goog.asserts.assert(e instanceof shaka.util.Error, 'Wrong error type');
  341. e.severity = shaka.util.Error.Severity.CRITICAL;
  342. }
  343. throw e;
  344. });
  345. // Return the pending request, which carries the response operation, and the
  346. // number of bytes remaining to be downloaded, updated by the progress
  347. // events. Add the operation to the manager for later cleanup.
  348. const pendingRequest =
  349. new shaka.net.NetworkingEngine.PendingRequest(
  350. op.promise, () => op.abort(), numBytesRemainingObj);
  351. this.operationManager_.manage(pendingRequest);
  352. return pendingRequest;
  353. }
  354. /**
  355. * @param {shaka.net.NetworkingEngine.RequestType} type
  356. * @param {shaka.extern.Request} request
  357. * @param {shaka.extern.RequestContext=} context
  358. * @return {!shaka.util.AbortableOperation.<undefined>}
  359. * @private
  360. */
  361. filterRequest_(type, request, context) {
  362. let filterOperation = shaka.util.AbortableOperation.completed(undefined);
  363. const applyFilter = (requestFilter) => {
  364. filterOperation = filterOperation.chain(() => {
  365. if (request.body) {
  366. // TODO: For v4.0 we should remove this or change to always pass a
  367. // Uint8Array. To make it easier for apps to write filters, it may be
  368. // better to always pass a Uint8Array so they know what they are
  369. // getting; but we shouldn't use ArrayBuffer since that would require
  370. // copying buffers if this is a partial view.
  371. request.body = shaka.util.BufferUtils.toArrayBuffer(request.body);
  372. }
  373. return requestFilter(type, request, context);
  374. });
  375. };
  376. if (this.onRequest_) {
  377. applyFilter(this.onRequest_);
  378. }
  379. for (const requestFilter of this.requestFilters_) {
  380. // Request filters are run sequentially.
  381. applyFilter(requestFilter);
  382. }
  383. // Catch any errors thrown by request filters, and substitute
  384. // them with a Shaka-native error.
  385. return filterOperation.chain(undefined, (e) => {
  386. if (e instanceof shaka.util.Error &&
  387. e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  388. // Don't change anything if the operation was aborted.
  389. throw e;
  390. }
  391. throw new shaka.util.Error(
  392. shaka.util.Error.Severity.CRITICAL,
  393. shaka.util.Error.Category.NETWORK,
  394. shaka.util.Error.Code.REQUEST_FILTER_ERROR, e);
  395. });
  396. }
  397. /**
  398. * @param {shaka.net.NetworkingEngine.RequestType} type
  399. * @param {shaka.extern.Request} request
  400. * @param {(shaka.extern.RequestContext|undefined)} context
  401. * @param {shaka.net.NetworkingEngine.NumBytesRemainingClass
  402. * } numBytesRemainingObj
  403. * @return {!shaka.extern.IAbortableOperation.<
  404. * shaka.net.NetworkingEngine.ResponseAndGotProgress>}
  405. * @private
  406. */
  407. makeRequestWithRetry_(type, request, context, numBytesRemainingObj) {
  408. const backoff = new shaka.net.Backoff(
  409. request.retryParameters, /* autoReset= */ false);
  410. const index = 0;
  411. return this.send_(
  412. type, request, context, backoff, index, /* lastError= */ null,
  413. numBytesRemainingObj);
  414. }
  415. /**
  416. * Sends the given request to the correct plugin and retry using Backoff.
  417. *
  418. * @param {shaka.net.NetworkingEngine.RequestType} type
  419. * @param {shaka.extern.Request} request
  420. * @param {(shaka.extern.RequestContext|undefined)} context
  421. * @param {!shaka.net.Backoff} backoff
  422. * @param {number} index
  423. * @param {?shaka.util.Error} lastError
  424. * @param {shaka.net.NetworkingEngine.NumBytesRemainingClass
  425. * } numBytesRemainingObj
  426. * @return {!shaka.extern.IAbortableOperation.<
  427. * shaka.net.NetworkingEngine.ResponseAndGotProgress>}
  428. * @private
  429. */
  430. send_(type, request, context, backoff, index, lastError,
  431. numBytesRemainingObj) {
  432. goog.asserts.assert(this.config_, 'Config must not be null!');
  433. if (this.config_.forceHTTP) {
  434. request.uris[index] = request.uris[index].replace('https://', 'http://');
  435. }
  436. if (this.config_.forceHTTPS) {
  437. request.uris[index] = request.uris[index].replace('http://', 'https://');
  438. }
  439. if (index > 0 && this.onRetry_) {
  440. const newUri = request.uris[index];
  441. const oldUri = request.uris[index - 1];
  442. this.onRetry_(type, context, newUri, oldUri);
  443. }
  444. const uri = new goog.Uri(request.uris[index]);
  445. let scheme = uri.getScheme();
  446. // Whether it got a progress event.
  447. let gotProgress = false;
  448. if (!scheme) {
  449. // If there is no scheme, infer one from the location.
  450. scheme = shaka.net.NetworkingEngine.getLocationProtocol_();
  451. goog.asserts.assert(
  452. scheme[scheme.length - 1] == ':',
  453. 'location.protocol expected to end with a colon!');
  454. // Drop the colon.
  455. scheme = scheme.slice(0, -1);
  456. // Override the original URI to make the scheme explicit.
  457. uri.setScheme(scheme);
  458. request.uris[index] = uri.toString();
  459. }
  460. // Schemes are meant to be case-insensitive.
  461. // See https://github.com/shaka-project/shaka-player/issues/2173
  462. // and https://tools.ietf.org/html/rfc3986#section-3.1
  463. scheme = scheme.toLowerCase();
  464. const object = shaka.net.NetworkingEngine.schemes_.get(scheme);
  465. const plugin = object ? object.plugin : null;
  466. if (!plugin) {
  467. return shaka.util.AbortableOperation.failed(
  468. new shaka.util.Error(
  469. shaka.util.Error.Severity.CRITICAL,
  470. shaka.util.Error.Category.NETWORK,
  471. shaka.util.Error.Code.UNSUPPORTED_SCHEME,
  472. uri));
  473. }
  474. const progressSupport = object.progressSupport;
  475. // Add headers from CommonAccessToken
  476. const commonAccessToken =
  477. this.hostCommonAccessTokenMap_.get(uri.getDomain());
  478. if (commonAccessToken) {
  479. request.headers[shaka.net.NetworkingEngine.CommonAccessTokenHeaderName_] =
  480. commonAccessToken;
  481. }
  482. // Every attempt must have an associated backoff.attempt() call so that the
  483. // accounting is correct.
  484. const backoffOperation =
  485. shaka.util.AbortableOperation.notAbortable(backoff.attempt());
  486. /** @type {?shaka.util.Timer} */
  487. let connectionTimer = null;
  488. /** @type {?shaka.util.Timer} */
  489. let stallTimer = null;
  490. let aborted = false;
  491. let headersReceivedCalled = false;
  492. let startTimeMs;
  493. const sendOperation = backoffOperation.chain(() => {
  494. if (this.destroyed_) {
  495. return shaka.util.AbortableOperation.aborted();
  496. }
  497. startTimeMs = Date.now();
  498. const segment = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  499. let packetNumber = 0;
  500. const progressUpdated = (time, bytes, numBytesRemaining) => {
  501. if (connectionTimer) {
  502. connectionTimer.stop();
  503. }
  504. if (stallTimer) {
  505. stallTimer.tickAfter(stallTimeoutMs / 1000);
  506. }
  507. if (this.onProgressUpdated_ && type == segment) {
  508. packetNumber++;
  509. request.packetNumber = packetNumber;
  510. const allowSwitch = this.allowSwitch_(context);
  511. this.onProgressUpdated_(time, bytes, allowSwitch, request, context);
  512. gotProgress = true;
  513. numBytesRemainingObj.setBytes(numBytesRemaining);
  514. }
  515. };
  516. const headersReceived = (headers) => {
  517. headersReceivedCalled = true;
  518. request.timeToFirstByte = Date.now() -
  519. /** @type {number} */ (request.requestStartTime);
  520. if (this.onHeadersReceived_) {
  521. this.onHeadersReceived_(headers, request, type);
  522. }
  523. };
  524. request.requestStartTime = Date.now();
  525. const requestPlugin = plugin(
  526. request.uris[index], request, type, progressUpdated, headersReceived,
  527. {
  528. minBytesForProgressEvents: this.config_.minBytesForProgressEvents,
  529. });
  530. if (!progressSupport) {
  531. return requestPlugin;
  532. }
  533. const connectionTimeoutMs = request.retryParameters.connectionTimeout;
  534. if (connectionTimeoutMs) {
  535. connectionTimer = new shaka.util.Timer(() => {
  536. aborted = true;
  537. requestPlugin.abort();
  538. });
  539. connectionTimer.tickAfter(connectionTimeoutMs / 1000);
  540. }
  541. const stallTimeoutMs = request.retryParameters.stallTimeout;
  542. if (stallTimeoutMs) {
  543. stallTimer = new shaka.util.Timer(() => {
  544. aborted = true;
  545. requestPlugin.abort();
  546. });
  547. }
  548. return requestPlugin;
  549. }).chain((response) => {
  550. if (connectionTimer) {
  551. connectionTimer.stop();
  552. }
  553. if (stallTimer) {
  554. stallTimer.stop();
  555. }
  556. if (response.timeMs == undefined) {
  557. response.timeMs = Date.now() - startTimeMs;
  558. }
  559. // Process headers to get the new CommonAccessToken
  560. const commonAccessTokenHeader = response.headers[
  561. shaka.net.NetworkingEngine.CommonAccessTokenHeaderName_];
  562. if (commonAccessTokenHeader) {
  563. const responseHost = new goog.Uri(response.uri);
  564. this.hostCommonAccessTokenMap_.set(
  565. responseHost.getDomain(), commonAccessTokenHeader);
  566. }
  567. const responseAndGotProgress = {
  568. response: response,
  569. gotProgress: gotProgress,
  570. };
  571. if (!headersReceivedCalled) {
  572. // The plugin did not call headersReceived, perhaps because it is not
  573. // able to track that information. So, fire the event manually.
  574. if (this.onHeadersReceived_) {
  575. this.onHeadersReceived_(response.headers, request, type);
  576. }
  577. }
  578. if (this.onDownloadCompleted_) {
  579. this.onDownloadCompleted_(request, response);
  580. }
  581. return responseAndGotProgress;
  582. }, (error) => {
  583. if (connectionTimer) {
  584. connectionTimer.stop();
  585. }
  586. if (stallTimer) {
  587. stallTimer.stop();
  588. }
  589. if (this.onDownloadFailed_) {
  590. let shakaError = null;
  591. let httpResponseCode = 0;
  592. if (error instanceof shaka.util.Error) {
  593. shakaError = error;
  594. if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
  595. httpResponseCode = /** @type {number} */ (error.data[1]);
  596. }
  597. }
  598. this.onDownloadFailed_(request, shakaError, httpResponseCode, aborted);
  599. }
  600. if (this.destroyed_) {
  601. return shaka.util.AbortableOperation.aborted();
  602. }
  603. if (aborted) {
  604. // It is necessary to change the error code to the correct one because
  605. // otherwise the retry logic would not work.
  606. error = new shaka.util.Error(
  607. shaka.util.Error.Severity.RECOVERABLE,
  608. shaka.util.Error.Category.NETWORK,
  609. shaka.util.Error.Code.TIMEOUT,
  610. request.uris[index], type);
  611. }
  612. if (error instanceof shaka.util.Error) {
  613. if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  614. // Don't change anything if the operation was aborted.
  615. throw error;
  616. } else if (error.code == shaka.util.Error.Code.ATTEMPTS_EXHAUSTED) {
  617. goog.asserts.assert(lastError, 'Should have last error');
  618. throw lastError;
  619. }
  620. if (error.severity == shaka.util.Error.Severity.RECOVERABLE) {
  621. const data = (new Map()).set('error', error);
  622. const event = new shaka.util.FakeEvent('retry', data);
  623. // A user can call preventDefault() on a cancelable event.
  624. event.cancelable = true;
  625. this.dispatchEvent(event);
  626. if (event.defaultPrevented) {
  627. // If the caller uses preventDefault() on the 'retry' event, don't
  628. // retry any more.
  629. throw error;
  630. }
  631. // Move to the next URI.
  632. index = (index + 1) % request.uris.length;
  633. return this.send_(
  634. type, request, context, backoff, index, error,
  635. numBytesRemainingObj);
  636. }
  637. }
  638. // The error was not recoverable, so do not try again.
  639. throw error;
  640. });
  641. return sendOperation;
  642. }
  643. /**
  644. * @param {shaka.net.NetworkingEngine.RequestType} type
  645. * @param {shaka.net.NetworkingEngine.ResponseAndGotProgress
  646. * } responseAndGotProgress
  647. * @param {shaka.extern.RequestContext=} context
  648. * @return {!shaka.extern.IAbortableOperation.<
  649. * shaka.net.NetworkingEngine.ResponseAndGotProgress>}
  650. * @private
  651. */
  652. filterResponse_(type, responseAndGotProgress, context) {
  653. let filterOperation = shaka.util.AbortableOperation.completed(undefined);
  654. for (const responseFilter of this.responseFilters_) {
  655. // Response filters are run sequentially.
  656. filterOperation = filterOperation.chain(() => {
  657. const resp = responseAndGotProgress.response;
  658. if (resp.data) {
  659. // TODO: See TODO in filterRequest_.
  660. resp.data = shaka.util.BufferUtils.toArrayBuffer(resp.data);
  661. }
  662. return responseFilter(type, resp, context);
  663. });
  664. }
  665. // If successful, return the filtered response with whether it got
  666. // progress.
  667. return filterOperation.chain(() => {
  668. return responseAndGotProgress;
  669. }, (e) => {
  670. // Catch any errors thrown by request filters, and substitute
  671. // them with a Shaka-native error.
  672. // The error is assumed to be critical if the original wasn't a Shaka
  673. // error.
  674. let severity = shaka.util.Error.Severity.CRITICAL;
  675. if (e instanceof shaka.util.Error) {
  676. if (e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  677. // Don't change anything if the operation was aborted.
  678. throw e;
  679. }
  680. severity = e.severity;
  681. }
  682. throw new shaka.util.Error(
  683. severity,
  684. shaka.util.Error.Category.NETWORK,
  685. shaka.util.Error.Code.RESPONSE_FILTER_ERROR, e);
  686. });
  687. }
  688. /**
  689. * @param {(shaka.extern.RequestContext|undefined)} context
  690. * @return {boolean}
  691. * @private
  692. */
  693. allowSwitch_(context) {
  694. if (context) {
  695. const segment = context.segment;
  696. const stream = context.stream;
  697. if (segment && stream && stream.fastSwitching) {
  698. if (segment.isPartial()) {
  699. return false;
  700. }
  701. }
  702. }
  703. return true;
  704. }
  705. /**
  706. * This is here only for testability. We can't mock location in our tests on
  707. * all browsers, so instead we mock this.
  708. *
  709. * @return {string} The value of location.protocol.
  710. * @private
  711. */
  712. static getLocationProtocol_() {
  713. return location.protocol;
  714. }
  715. };
  716. /**
  717. * A wrapper class for the number of bytes remaining to be downloaded for the
  718. * request.
  719. * Instead of using PendingRequest directly, this class is needed to be sent to
  720. * plugin as a parameter, and a Promise is returned, before PendingRequest is
  721. * created.
  722. *
  723. * @export
  724. */
  725. shaka.net.NetworkingEngine.NumBytesRemainingClass = class {
  726. /**
  727. * Constructor
  728. */
  729. constructor() {
  730. /** @private {number} */
  731. this.bytesToLoad_ = 0;
  732. }
  733. /**
  734. * @param {number} bytesToLoad
  735. */
  736. setBytes(bytesToLoad) {
  737. this.bytesToLoad_ = bytesToLoad;
  738. }
  739. /**
  740. * @return {number}
  741. */
  742. getBytes() {
  743. return this.bytesToLoad_;
  744. }
  745. };
  746. /**
  747. * A pending network request. This can track the current progress of the
  748. * download, and allows the request to be aborted if the network is slow.
  749. *
  750. * @implements {shaka.extern.IAbortableOperation.<shaka.extern.Response>}
  751. * @extends {shaka.util.AbortableOperation}
  752. * @export
  753. */
  754. shaka.net.NetworkingEngine.PendingRequest =
  755. class extends shaka.util.AbortableOperation {
  756. /**
  757. * @param {!Promise} promise
  758. * A Promise which represents the underlying operation. It is resolved
  759. * when the operation is complete, and rejected if the operation fails or
  760. * is aborted. Aborted operations should be rejected with a
  761. * shaka.util.Error object using the error code OPERATION_ABORTED.
  762. * @param {function():!Promise} onAbort
  763. * Will be called by this object to abort the underlying operation. This
  764. * is not cancellation, and will not necessarily result in any work being
  765. * undone. abort() should return a Promise which is resolved when the
  766. * underlying operation has been aborted. The returned Promise should
  767. * never be rejected.
  768. * @param {shaka.net.NetworkingEngine.NumBytesRemainingClass
  769. * } numBytesRemainingObj
  770. */
  771. constructor(promise, onAbort, numBytesRemainingObj) {
  772. super(promise, onAbort);
  773. /** @private {shaka.net.NetworkingEngine.NumBytesRemainingClass} */
  774. this.bytesRemaining_ = numBytesRemainingObj;
  775. }
  776. /**
  777. * @return {number}
  778. */
  779. getBytesRemaining() {
  780. return this.bytesRemaining_.getBytes();
  781. }
  782. };
  783. /**
  784. * @const {string}
  785. * @private
  786. */
  787. shaka.net.NetworkingEngine.CommonAccessTokenHeaderName_ =
  788. 'common-access-token';
  789. /**
  790. * Request types. Allows a filter to decide which requests to read/alter.
  791. *
  792. * @enum {number}
  793. * @export
  794. */
  795. shaka.net.NetworkingEngine.RequestType = {
  796. 'MANIFEST': 0,
  797. 'SEGMENT': 1,
  798. 'LICENSE': 2,
  799. 'APP': 3,
  800. 'TIMING': 4,
  801. 'SERVER_CERTIFICATE': 5,
  802. 'KEY': 6,
  803. 'ADS': 7,
  804. 'CONTENT_STEERING': 8,
  805. };
  806. /**
  807. * A more advanced form of the RequestType structure, meant to describe
  808. * sub-types of basic request types.
  809. * For example, an INIT_SEGMENT is a sub-type of SEGMENT.
  810. * This is meant to allow for more specificity to be added to the request type
  811. * data, without breaking backwards compatibility.
  812. *
  813. * @enum {number}
  814. * @export
  815. */
  816. shaka.net.NetworkingEngine.AdvancedRequestType = {
  817. 'INIT_SEGMENT': 0,
  818. 'MEDIA_SEGMENT': 1,
  819. 'MEDIA_PLAYLIST': 2,
  820. 'MASTER_PLAYLIST': 3,
  821. 'MPD': 4,
  822. 'MSS': 5,
  823. 'MPD_PATCH': 6,
  824. 'MEDIATAILOR_SESSION_INFO': 7,
  825. 'MEDIATAILOR_TRACKING_INFO': 8,
  826. 'MEDIATAILOR_STATIC_RESOURCE': 9,
  827. 'MEDIATAILOR_TRACKING_EVENT': 10,
  828. 'INTERSTITIAL_ASSET_LIST': 11,
  829. 'INTERSTITIAL_AD_URL': 12,
  830. };
  831. /**
  832. * Priority level for network scheme plugins.
  833. * If multiple plugins are provided for the same scheme, only the
  834. * highest-priority one is used.
  835. *
  836. * @enum {number}
  837. * @export
  838. */
  839. shaka.net.NetworkingEngine.PluginPriority = {
  840. 'FALLBACK': 1,
  841. 'PREFERRED': 2,
  842. 'APPLICATION': 3,
  843. };
  844. /**
  845. * @typedef {{
  846. * plugin: shaka.extern.SchemePlugin,
  847. * priority: number,
  848. * progressSupport: boolean
  849. * }}
  850. * @property {shaka.extern.SchemePlugin} plugin
  851. * The associated plugin.
  852. * @property {number} priority
  853. * The plugin's priority.
  854. * @property {boolean} progressSupport
  855. * The plugin's supports progress events
  856. */
  857. shaka.net.NetworkingEngine.SchemeObject;
  858. /**
  859. * Contains the scheme plugins.
  860. *
  861. * @private {!Map<string, shaka.net.NetworkingEngine.SchemeObject>}
  862. */
  863. shaka.net.NetworkingEngine.schemes_ = new Map();
  864. /**
  865. * @typedef {{
  866. * response: shaka.extern.Response,
  867. * gotProgress: boolean
  868. * }}
  869. *
  870. * @description
  871. * Defines a response wrapper object, including the response object and whether
  872. * progress event is fired by the scheme plugin.
  873. *
  874. * @property {shaka.extern.Response} response
  875. * @property {boolean} gotProgress
  876. * @private
  877. */
  878. shaka.net.NetworkingEngine.ResponseAndGotProgress;
  879. /**
  880. * @typedef {function(
  881. * !Object<string, string>,
  882. * !shaka.extern.Request,
  883. * !shaka.net.NetworkingEngine.RequestType)}
  884. *
  885. * @description
  886. * A callback function that passes the shaka.extern.HeadersReceived along to
  887. * the player, plus some extra data.
  888. * @export
  889. */
  890. shaka.net.NetworkingEngine.OnHeadersReceived;
  891. /**
  892. * @typedef {function(
  893. * number,
  894. * number,
  895. * boolean,
  896. * shaka.extern.Request=,
  897. * shaka.extern.RequestContext=)}
  898. *
  899. * @description
  900. * A callback that is passed the duration, in milliseconds,
  901. * that the request took, the number of bytes transferred, a boolean
  902. * representing whether the switching is allowed and a ref to the
  903. * original request.
  904. * @export
  905. */
  906. shaka.net.NetworkingEngine.onProgressUpdated;
  907. /**
  908. * @typedef {function(
  909. * !shaka.extern.Request,
  910. * !shaka.extern.Response)}
  911. *
  912. * @description
  913. * A callback function that notifies the player when a download completed
  914. * successfully.
  915. * @export
  916. */
  917. shaka.net.NetworkingEngine.OnDownloadCompleted;
  918. /**
  919. * @typedef {function(
  920. * !shaka.extern.Request,
  921. * ?shaka.util.Error,
  922. * number,
  923. * boolean)}
  924. *
  925. * @description
  926. * A callback function that notifies the player when a download fails, for any
  927. * reason (e.g. even if the download was aborted).
  928. * @export
  929. */
  930. shaka.net.NetworkingEngine.OnDownloadFailed;
  931. /**
  932. * @typedef {function(
  933. * !shaka.net.NetworkingEngine.RequestType,
  934. * !shaka.extern.Request,
  935. * (shaka.extern.RequestContext|undefined))}
  936. *
  937. * @description
  938. * A callback function called on every request
  939. * @export
  940. */
  941. shaka.net.NetworkingEngine.OnRequest;
  942. /**
  943. * @typedef {function(
  944. * !shaka.net.NetworkingEngine.RequestType,
  945. * (shaka.extern.RequestContext|undefined),
  946. * string,
  947. * string)}
  948. *
  949. * @description
  950. * A callback function called on every request retry. The first string is the
  951. * new URI and the second string is the old URI.
  952. * @export
  953. */
  954. shaka.net.NetworkingEngine.OnRetry;
  955. /**
  956. * @typedef {function(
  957. * !shaka.net.NetworkingEngine.RequestType,
  958. * !shaka.extern.Response,
  959. * (shaka.extern.RequestContext|undefined))}
  960. *
  961. * @description
  962. * A callback function called on every request
  963. * @export
  964. */
  965. shaka.net.NetworkingEngine.OnResponse;