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