Source: lib/net/http_xhr_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.HttpXHRPlugin');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.net.HttpPluginUtils');
  9. goog.require('shaka.net.NetworkingEngine');
  10. goog.require('shaka.util.AbortableOperation');
  11. goog.require('shaka.util.Error');
  12. /**
  13. * @summary A networking plugin to handle http and https URIs via XHR.
  14. * @export
  15. */
  16. shaka.net.HttpXHRPlugin = class {
  17. /**
  18. * @param {string} uri
  19. * @param {shaka.extern.Request} request
  20. * @param {shaka.net.NetworkingEngine.RequestType} requestType
  21. * @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
  22. * progress event happened.
  23. * @param {shaka.extern.HeadersReceived} headersReceived Called when the
  24. * headers for the download are received, but before the body is.
  25. * @param {shaka.extern.SchemePluginConfig} config
  26. * @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
  27. * @export
  28. */
  29. static parse(uri, request, requestType, progressUpdated, headersReceived,
  30. config) {
  31. const xhr = new shaka.net.HttpXHRPlugin.Xhr_();
  32. // Last time stamp when we got a progress event.
  33. let lastTime = Date.now();
  34. // Last number of bytes loaded, from progress event.
  35. let lastLoaded = 0;
  36. const promise = new Promise(((resolve, reject) => {
  37. xhr.open(request.method, uri, true);
  38. xhr.responseType = 'arraybuffer';
  39. xhr.timeout = request.retryParameters.timeout;
  40. xhr.withCredentials = request.allowCrossSiteCredentials;
  41. let headers = {};
  42. xhr.onabort = () => {
  43. reject(new shaka.util.Error(
  44. shaka.util.Error.Severity.RECOVERABLE,
  45. shaka.util.Error.Category.NETWORK,
  46. shaka.util.Error.Code.OPERATION_ABORTED,
  47. uri, requestType));
  48. };
  49. xhr.onreadystatechange = (event) => {
  50. if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
  51. headers = shaka.net.HttpXHRPlugin.headersToGenericObject_(xhr);
  52. headersReceived(headers);
  53. }
  54. };
  55. xhr.onload = (event) => {
  56. // eslint-disable-next-line shaka-rules/buffersource-no-instanceof
  57. goog.asserts.assert(xhr.response instanceof ArrayBuffer,
  58. 'XHR should have a response by now!');
  59. const xhrResponse = xhr.response;
  60. try {
  61. const currentTime = Date.now();
  62. progressUpdated(currentTime - lastTime, event.loaded - lastLoaded,
  63. /* numBytesRemaining= */ 0);
  64. const response = shaka.net.HttpPluginUtils.makeResponse(headers,
  65. xhrResponse, xhr.status, uri, xhr.responseURL, request,
  66. requestType);
  67. resolve(response);
  68. } catch (error) {
  69. goog.asserts.assert(error instanceof shaka.util.Error,
  70. 'Wrong error type!');
  71. reject(error);
  72. }
  73. };
  74. xhr.onerror = (event) => {
  75. reject(new shaka.util.Error(
  76. shaka.util.Error.Severity.RECOVERABLE,
  77. shaka.util.Error.Category.NETWORK,
  78. shaka.util.Error.Code.HTTP_ERROR,
  79. uri, event, requestType));
  80. };
  81. xhr.ontimeout = (event) => {
  82. reject(new shaka.util.Error(
  83. shaka.util.Error.Severity.RECOVERABLE,
  84. shaka.util.Error.Category.NETWORK,
  85. shaka.util.Error.Code.TIMEOUT,
  86. uri, requestType));
  87. };
  88. xhr.onprogress = (event) => {
  89. const currentTime = Date.now();
  90. // If the time between last time and this time we got progress event
  91. // is long enough, or if a whole segment is downloaded, call
  92. // progressUpdated().
  93. const minBytes = config.minBytesForProgressEvents || 0;
  94. const chunkSize = event.loaded - lastLoaded;
  95. if ((currentTime - lastTime > 100 && chunkSize >= minBytes) ||
  96. (event.lengthComputable && event.loaded == event.total)) {
  97. const numBytesRemaining =
  98. xhr.readyState == 4 ? 0 : event.total - event.loaded;
  99. progressUpdated(currentTime - lastTime, chunkSize,
  100. numBytesRemaining);
  101. lastLoaded = event.loaded;
  102. lastTime = currentTime;
  103. }
  104. };
  105. for (const key in request.headers) {
  106. // The Fetch API automatically normalizes outgoing header keys to
  107. // lowercase. For consistency's sake, do it here too.
  108. const lowercasedKey = key.toLowerCase();
  109. xhr.setRequestHeader(lowercasedKey, request.headers[key]);
  110. }
  111. xhr.send(request.body);
  112. }));
  113. return new shaka.util.AbortableOperation(
  114. promise,
  115. () => {
  116. xhr.abort();
  117. return Promise.resolve();
  118. });
  119. }
  120. /**
  121. * @param {!XMLHttpRequest} xhr
  122. * @return {!Object<string, string>}
  123. * @private
  124. */
  125. static headersToGenericObject_(xhr) {
  126. // Since Edge incorrectly return the header with a leading new
  127. // line character ('\n'), we trim the header here.
  128. const headerLines = xhr.getAllResponseHeaders().trim().split('\r\n');
  129. const headers = {};
  130. for (const header of headerLines) {
  131. /** @type {!Array<string>} */
  132. const parts = header.split(': ');
  133. headers[parts[0].toLowerCase()] = parts.slice(1).join(': ');
  134. }
  135. return headers;
  136. }
  137. };
  138. /**
  139. * Overridden in unit tests, but compiled out in production.
  140. *
  141. * @const {function(new: XMLHttpRequest)}
  142. * @private
  143. */
  144. shaka.net.HttpXHRPlugin.Xhr_ = window.XMLHttpRequest;
  145. shaka.net.NetworkingEngine.registerScheme(
  146. 'http', shaka.net.HttpXHRPlugin.parse,
  147. shaka.net.NetworkingEngine.PluginPriority.FALLBACK,
  148. /* progressSupport= */ true);
  149. shaka.net.NetworkingEngine.registerScheme(
  150. 'https', shaka.net.HttpXHRPlugin.parse,
  151. shaka.net.NetworkingEngine.PluginPriority.FALLBACK,
  152. /* progressSupport= */ true);
  153. shaka.net.NetworkingEngine.registerScheme(
  154. 'blob', shaka.net.HttpXHRPlugin.parse,
  155. shaka.net.NetworkingEngine.PluginPriority.FALLBACK,
  156. /* progressSupport= */ true);