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