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