Home Reference Source

src/utils/xhr-loader.ts

  1. import { logger } from '../utils/logger';
  2. import type {
  3. LoaderCallbacks,
  4. LoaderContext,
  5. LoaderStats,
  6. Loader,
  7. LoaderConfiguration,
  8. } from '../types/loader';
  9. import { LoadStats } from '../loader/load-stats';
  10.  
  11. class XhrLoader implements Loader<LoaderContext> {
  12. private xhrSetup: Function | null;
  13. private requestTimeout?: number;
  14. private retryTimeout?: number;
  15. private retryDelay: number;
  16. private config: LoaderConfiguration | null = null;
  17. private callbacks: LoaderCallbacks<LoaderContext> | null = null;
  18. public context!: LoaderContext;
  19.  
  20. public loader: XMLHttpRequest | null = null;
  21. public stats: LoaderStats;
  22.  
  23. constructor(config /* HlsConfig */) {
  24. this.xhrSetup = config ? config.xhrSetup : null;
  25. this.stats = new LoadStats();
  26. this.retryDelay = 0;
  27. }
  28.  
  29. destroy(): void {
  30. this.callbacks = null;
  31. this.abortInternal();
  32. this.loader = null;
  33. this.config = null;
  34. }
  35.  
  36. abortInternal(): void {
  37. const loader = this.loader;
  38. if (loader && loader.readyState !== 4) {
  39. this.stats.aborted = true;
  40. loader.abort();
  41. }
  42. self.clearTimeout(this.requestTimeout);
  43. self.clearTimeout(this.retryTimeout);
  44. }
  45.  
  46. abort(): void {
  47. this.abortInternal();
  48. if (this.callbacks?.onAbort) {
  49. this.callbacks.onAbort(this.stats, this.context, this.loader);
  50. }
  51. }
  52.  
  53. load(
  54. context: LoaderContext,
  55. config: LoaderConfiguration,
  56. callbacks: LoaderCallbacks<LoaderContext>
  57. ): void {
  58. if (this.stats.loading.start) {
  59. throw new Error('Loader can only be used once.');
  60. }
  61. this.stats.loading.start = self.performance.now();
  62. this.context = context;
  63. this.config = config;
  64. this.callbacks = callbacks;
  65. this.retryDelay = config.retryDelay;
  66. this.loadInternal();
  67. }
  68.  
  69. loadInternal(): void {
  70. const { config, context } = this;
  71. if (!config) {
  72. return;
  73. }
  74. const xhr = (this.loader = new self.XMLHttpRequest());
  75.  
  76. const stats = this.stats;
  77. stats.loading.first = 0;
  78. stats.loaded = 0;
  79. const xhrSetup = this.xhrSetup;
  80.  
  81. try {
  82. if (xhrSetup) {
  83. try {
  84. xhrSetup(xhr, context.url);
  85. } catch (e) {
  86. // fix xhrSetup: (xhr, url) => {xhr.setRequestHeader("Content-Language", "test");}
  87. // not working, as xhr.setRequestHeader expects xhr.readyState === OPEN
  88. xhr.open('GET', context.url, true);
  89. xhrSetup(xhr, context.url);
  90. }
  91. }
  92. if (!xhr.readyState) {
  93. xhr.open('GET', context.url, true);
  94. }
  95. } catch (e) {
  96. // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS
  97. this.callbacks!.onError(
  98. { code: xhr.status, text: e.message },
  99. context,
  100. xhr
  101. );
  102. return;
  103. }
  104.  
  105. if (context.rangeEnd) {
  106. xhr.setRequestHeader(
  107. 'Range',
  108. 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1)
  109. );
  110. }
  111.  
  112. xhr.onreadystatechange = this.readystatechange.bind(this);
  113. xhr.onprogress = this.loadprogress.bind(this);
  114. xhr.responseType = context.responseType as XMLHttpRequestResponseType;
  115. // setup timeout before we perform request
  116. self.clearTimeout(this.requestTimeout);
  117. this.requestTimeout = self.setTimeout(
  118. this.loadtimeout.bind(this),
  119. config.timeout
  120. );
  121. xhr.send();
  122. }
  123.  
  124. readystatechange(): void {
  125. const { context, loader: xhr, stats } = this;
  126. if (!context || !xhr) {
  127. return;
  128. }
  129. const readyState = xhr.readyState;
  130. const config = this.config as LoaderConfiguration;
  131.  
  132. // don't proceed if xhr has been aborted
  133. if (stats.aborted) {
  134. return;
  135. }
  136.  
  137. // >= HEADERS_RECEIVED
  138. if (readyState >= 2) {
  139. // clear xhr timeout and rearm it if readyState less than 4
  140. self.clearTimeout(this.requestTimeout);
  141. if (stats.loading.first === 0) {
  142. stats.loading.first = Math.max(
  143. self.performance.now(),
  144. stats.loading.start
  145. );
  146. }
  147.  
  148. if (readyState === 4) {
  149. const status = xhr.status;
  150. // http status between 200 to 299 are all successful
  151. if (status >= 200 && status < 300) {
  152. stats.loading.end = Math.max(
  153. self.performance.now(),
  154. stats.loading.first
  155. );
  156. let data;
  157. let len: number;
  158. if (context.responseType === 'arraybuffer') {
  159. data = xhr.response;
  160. len = data.byteLength;
  161. } else {
  162. data = xhr.responseText;
  163. len = data.length;
  164. }
  165. stats.loaded = stats.total = len;
  166.  
  167. const onProgress = this.callbacks!.onProgress;
  168. if (onProgress) {
  169. onProgress(stats, context, data, xhr);
  170. }
  171.  
  172. const response = {
  173. url: xhr.responseURL,
  174. data: data,
  175. };
  176.  
  177. this.callbacks!.onSuccess(response, stats, context, xhr);
  178. } else {
  179. // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error
  180. if (
  181. stats.retry >= config.maxRetry ||
  182. (status >= 400 && status < 499)
  183. ) {
  184. logger.error(`${status} while loading ${context.url}`);
  185. this.callbacks!.onError(
  186. { code: status, text: xhr.statusText },
  187. context,
  188. xhr
  189. );
  190. } else {
  191. // retry
  192. logger.warn(
  193. `${status} while loading ${context.url}, retrying in ${this.retryDelay}...`
  194. );
  195. // abort and reset internal state
  196. this.abortInternal();
  197. this.loader = null;
  198. // schedule retry
  199. self.clearTimeout(this.retryTimeout);
  200. this.retryTimeout = self.setTimeout(
  201. this.loadInternal.bind(this),
  202. this.retryDelay
  203. );
  204. // set exponential backoff
  205. this.retryDelay = Math.min(
  206. 2 * this.retryDelay,
  207. config.maxRetryDelay
  208. );
  209. stats.retry++;
  210. }
  211. }
  212. } else {
  213. // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet
  214. self.clearTimeout(this.requestTimeout);
  215. this.requestTimeout = self.setTimeout(
  216. this.loadtimeout.bind(this),
  217. config.timeout
  218. );
  219. }
  220. }
  221. }
  222.  
  223. loadtimeout(): void {
  224. logger.warn(`timeout while loading ${this.context.url}`);
  225. const callbacks = this.callbacks;
  226. if (callbacks) {
  227. this.abortInternal();
  228. callbacks.onTimeout(this.stats, this.context, this.loader);
  229. }
  230. }
  231.  
  232. loadprogress(event: ProgressEvent): void {
  233. const stats = this.stats;
  234.  
  235. stats.loaded = event.loaded;
  236. if (event.lengthComputable) {
  237. stats.total = event.total;
  238. }
  239. }
  240.  
  241. getResponseHeader(name: string): string | null {
  242. if (this.loader) {
  243. try {
  244. return this.loader.getResponseHeader(name);
  245. } catch (error) {
  246. /* Could not get headers */
  247. }
  248. }
  249. return null;
  250. }
  251. }
  252.  
  253. export default XhrLoader;