Source: lib/net/http_fetch_plugin.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.net.HttpFetchPlugin');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.net.HttpPluginUtils');
  10. goog.require('shaka.net.NetworkingEngine');
  11. goog.require('shaka.util.AbortableOperation');
  12. goog.require('shaka.util.Error');
  13. goog.require('shaka.util.MapUtils');
  14. goog.require('shaka.util.Timer');
  15. /**
  16. * @summary A networking plugin to handle http and https URIs via the Fetch API.
  17. * @export
  18. */
  19. shaka.net.HttpFetchPlugin = class {
  20. /**
  21. * @param {string} uri
  22. * @param {shaka.extern.Request} request
  23. * @param {shaka.net.NetworkingEngine.RequestType} requestType
  24. * @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
  25. * progress event happened.
  26. * @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
  27. * @export
  28. */
  29. static parse(uri, request, requestType, progressUpdated) {
  30. const headers = new shaka.net.HttpFetchPlugin.Headers_();
  31. shaka.util.MapUtils.asMap(request.headers).forEach((value, key) => {
  32. headers.append(key, value);
  33. });
  34. const controller = new shaka.net.HttpFetchPlugin.AbortController_();
  35. /** @type {!RequestInit} */
  36. const init = {
  37. // Edge does not treat null as undefined for body; https://bit.ly/2luyE6x
  38. body: request.body || undefined,
  39. headers: headers,
  40. method: request.method,
  41. signal: controller.signal,
  42. credentials: request.allowCrossSiteCredentials ? 'include' : undefined,
  43. };
  44. /** @type {shaka.net.HttpFetchPlugin.AbortStatus} */
  45. const abortStatus = {
  46. canceled: false,
  47. timedOut: false,
  48. };
  49. const pendingRequest = shaka.net.HttpFetchPlugin.request_(
  50. uri, requestType, init, abortStatus, progressUpdated,
  51. request.streamDataCallback);
  52. /** @type {!shaka.util.AbortableOperation} */
  53. const op = new shaka.util.AbortableOperation(pendingRequest, () => {
  54. abortStatus.canceled = true;
  55. controller.abort();
  56. return Promise.resolve();
  57. });
  58. // The fetch API does not timeout natively, so do a timeout manually using
  59. // the AbortController.
  60. const timeoutMs = request.retryParameters.timeout;
  61. if (timeoutMs) {
  62. const timer = new shaka.util.Timer(() => {
  63. abortStatus.timedOut = true;
  64. controller.abort();
  65. });
  66. timer.tickAfter(timeoutMs / 1000);
  67. // To avoid calling |abort| on the network request after it finished, we
  68. // will stop the timer when the requests resolves/rejects.
  69. op.finally(() => {
  70. timer.stop();
  71. });
  72. }
  73. return op;
  74. }
  75. /**
  76. * @param {string} uri
  77. * @param {shaka.net.NetworkingEngine.RequestType} requestType
  78. * @param {!RequestInit} init
  79. * @param {shaka.net.HttpFetchPlugin.AbortStatus} abortStatus
  80. * @param {shaka.extern.ProgressUpdated} progressUpdated
  81. * @param {?function(BufferSource):!Promise} streamDataCallback
  82. * @return {!Promise<!shaka.extern.Response>}
  83. * @private
  84. */
  85. static async request_(uri, requestType, init, abortStatus, progressUpdated,
  86. streamDataCallback) {
  87. const fetch = shaka.net.HttpFetchPlugin.fetch_;
  88. const ReadableStream = shaka.net.HttpFetchPlugin.ReadableStream_;
  89. let response;
  90. let arrayBuffer;
  91. let loaded = 0;
  92. let lastLoaded = 0;
  93. // Last time stamp when we got a progress event.
  94. let lastTime = Date.now();
  95. try {
  96. // The promise returned by fetch resolves as soon as the HTTP response
  97. // headers are available. The download itself isn't done until the promise
  98. // for retrieving the data (arrayBuffer, blob, etc) has resolved.
  99. response = await fetch(uri, init);
  100. // Getting the reader in this way allows us to observe the process of
  101. // downloading the body, instead of just waiting for an opaque promise to
  102. // resolve.
  103. // We first clone the response because calling getReader locks the body
  104. // stream; if we didn't clone it here, we would be unable to get the
  105. // response's arrayBuffer later.
  106. const reader = response.clone().body.getReader();
  107. const contentLengthRaw = response.headers.get('Content-Length');
  108. const contentLength =
  109. contentLengthRaw ? parseInt(contentLengthRaw, 10) : 0;
  110. const start = (controller) => {
  111. const push = async () => {
  112. let readObj;
  113. try {
  114. readObj = await reader.read();
  115. } catch (e) {
  116. // If we abort the request, we'll get an error here. Just ignore it
  117. // since real errors will be reported when we read the buffer below.
  118. shaka.log.v1('error reading from stream', e.message);
  119. return;
  120. }
  121. if (!readObj.done) {
  122. loaded += readObj.value.byteLength;
  123. if (streamDataCallback) {
  124. await streamDataCallback(readObj.value);
  125. }
  126. }
  127. const currentTime = Date.now();
  128. // If the time between last time and this time we got progress event
  129. // is long enough, or if a whole segment is downloaded, call
  130. // progressUpdated().
  131. if (currentTime - lastTime > 100 || readObj.done) {
  132. progressUpdated(currentTime - lastTime, loaded - lastLoaded,
  133. contentLength - loaded);
  134. lastLoaded = loaded;
  135. lastTime = currentTime;
  136. }
  137. if (readObj.done) {
  138. goog.asserts.assert(!readObj.value,
  139. 'readObj should be unset when "done" is true.');
  140. controller.close();
  141. } else {
  142. controller.enqueue(readObj.value);
  143. push();
  144. }
  145. };
  146. push();
  147. };
  148. // Create a ReadableStream to use the reader. We don't need to use the
  149. // actual stream for anything, though, as we are using the response's
  150. // arrayBuffer method to get the body, so we don't store the
  151. // ReadableStream.
  152. new ReadableStream({start}); // eslint-disable-line no-new
  153. arrayBuffer = await response.arrayBuffer();
  154. } catch (error) {
  155. if (abortStatus.canceled) {
  156. throw new shaka.util.Error(
  157. shaka.util.Error.Severity.RECOVERABLE,
  158. shaka.util.Error.Category.NETWORK,
  159. shaka.util.Error.Code.OPERATION_ABORTED,
  160. uri, requestType);
  161. } else if (abortStatus.timedOut) {
  162. throw new shaka.util.Error(
  163. shaka.util.Error.Severity.RECOVERABLE,
  164. shaka.util.Error.Category.NETWORK,
  165. shaka.util.Error.Code.TIMEOUT,
  166. uri, requestType);
  167. } else {
  168. throw new shaka.util.Error(
  169. shaka.util.Error.Severity.RECOVERABLE,
  170. shaka.util.Error.Category.NETWORK,
  171. shaka.util.Error.Code.HTTP_ERROR,
  172. uri, error, requestType);
  173. }
  174. }
  175. const headers = {};
  176. /** @type {Headers} */
  177. const responseHeaders = response.headers;
  178. responseHeaders.forEach((value, key) => {
  179. // Since Edge incorrectly return the header with a leading new line
  180. // character ('\n'), we trim the header here.
  181. headers[key.trim()] = value;
  182. });
  183. return shaka.net.HttpPluginUtils.makeResponse(
  184. headers, arrayBuffer, response.status, uri, response.url, requestType);
  185. }
  186. /**
  187. * Determine if the Fetch API is supported in the browser. Note: this is
  188. * deliberately exposed as a method to allow the client app to use the same
  189. * logic as Shaka when determining support.
  190. * @return {boolean}
  191. * @export
  192. */
  193. static isSupported() {
  194. // On Edge, ReadableStream exists, but attempting to construct it results in
  195. // an error. See https://bit.ly/2zwaFLL
  196. // So this has to check that ReadableStream is present AND usable.
  197. if (window.ReadableStream) {
  198. try {
  199. new ReadableStream({}); // eslint-disable-line no-new
  200. } catch (e) {
  201. return false;
  202. }
  203. } else {
  204. return false;
  205. }
  206. return !!(window.fetch && window.AbortController);
  207. }
  208. };
  209. /**
  210. * @typedef {{
  211. * canceled: boolean,
  212. * timedOut: boolean
  213. * }}
  214. * @property {boolean} canceled
  215. * Indicates if the request was canceled.
  216. * @property {boolean} timedOut
  217. * Indicates if the request timed out.
  218. */
  219. shaka.net.HttpFetchPlugin.AbortStatus;
  220. /**
  221. * Overridden in unit tests, but compiled out in production.
  222. *
  223. * @const {function(string, !RequestInit)}
  224. * @private
  225. */
  226. shaka.net.HttpFetchPlugin.fetch_ = window.fetch;
  227. /**
  228. * Overridden in unit tests, but compiled out in production.
  229. *
  230. * @const {function(new: AbortController)}
  231. * @private
  232. */
  233. shaka.net.HttpFetchPlugin.AbortController_ = window.AbortController;
  234. /**
  235. * Overridden in unit tests, but compiled out in production.
  236. *
  237. * @const {function(new: ReadableStream, !Object)}
  238. * @private
  239. */
  240. shaka.net.HttpFetchPlugin.ReadableStream_ = window.ReadableStream;
  241. /**
  242. * Overridden in unit tests, but compiled out in production.
  243. *
  244. * @const {function(new: Headers)}
  245. * @private
  246. */
  247. shaka.net.HttpFetchPlugin.Headers_ = window.Headers;
  248. if (shaka.net.HttpFetchPlugin.isSupported()) {
  249. shaka.net.NetworkingEngine.registerScheme(
  250. 'http', shaka.net.HttpFetchPlugin.parse,
  251. shaka.net.NetworkingEngine.PluginPriority.PREFERRED,
  252. /* progressSupport= */ true);
  253. shaka.net.NetworkingEngine.registerScheme(
  254. 'https', shaka.net.HttpFetchPlugin.parse,
  255. shaka.net.NetworkingEngine.PluginPriority.PREFERRED,
  256. /* progressSupport= */ true);
  257. }