Home Reference Source

src/utils/fetch-loader.ts

  1. import {
  2. LoaderCallbacks,
  3. LoaderContext,
  4. Loader,
  5. LoaderStats,
  6. LoaderConfiguration,
  7. LoaderOnProgress,
  8. } from '../types/loader';
  9. import { LoadStats } from '../loader/load-stats';
  10. import ChunkCache from '../demux/chunk-cache';
  11.  
  12. export function fetchSupported() {
  13. if (
  14. // @ts-ignore
  15. self.fetch &&
  16. self.AbortController &&
  17. self.ReadableStream &&
  18. self.Request
  19. ) {
  20. try {
  21. new self.ReadableStream({}); // eslint-disable-line no-new
  22. return true;
  23. } catch (e) {
  24. /* noop */
  25. }
  26. }
  27. return false;
  28. }
  29.  
  30. class FetchLoader implements Loader<LoaderContext> {
  31. private fetchSetup: Function;
  32. private requestTimeout?: number;
  33. private request!: Request;
  34. private response!: Response;
  35. private controller: AbortController;
  36. public context!: LoaderContext;
  37. private config: LoaderConfiguration | null = null;
  38. private callbacks: LoaderCallbacks<LoaderContext> | null = null;
  39. public stats: LoaderStats;
  40. private loader: Response | null = null;
  41.  
  42. constructor(config /* HlsConfig */) {
  43. this.fetchSetup = config.fetchSetup || getRequest;
  44. this.controller = new self.AbortController();
  45. this.stats = new LoadStats();
  46. }
  47.  
  48. destroy(): void {
  49. this.loader = this.callbacks = null;
  50. this.abortInternal();
  51. }
  52.  
  53. abortInternal(): void {
  54. const response = this.response;
  55. if (!response || !response.ok) {
  56. this.stats.aborted = true;
  57. this.controller.abort();
  58. }
  59. }
  60.  
  61. abort(): void {
  62. this.abortInternal();
  63. if (this.callbacks?.onAbort) {
  64. this.callbacks.onAbort(this.stats, this.context, this.response);
  65. }
  66. }
  67.  
  68. load(
  69. context: LoaderContext,
  70. config: LoaderConfiguration,
  71. callbacks: LoaderCallbacks<LoaderContext>
  72. ): void {
  73. const stats = this.stats;
  74. if (stats.loading.start) {
  75. throw new Error('Loader can only be used once.');
  76. }
  77. stats.loading.start = self.performance.now();
  78.  
  79. const initParams = getRequestParameters(context, this.controller.signal);
  80. const onProgress: LoaderOnProgress<LoaderContext> | undefined =
  81. callbacks.onProgress;
  82. const isArrayBuffer = context.responseType === 'arraybuffer';
  83. const LENGTH = isArrayBuffer ? 'byteLength' : 'length';
  84.  
  85. this.context = context;
  86. this.config = config;
  87. this.callbacks = callbacks;
  88. this.request = this.fetchSetup(context, initParams);
  89. self.clearTimeout(this.requestTimeout);
  90. this.requestTimeout = self.setTimeout(() => {
  91. this.abortInternal();
  92. callbacks.onTimeout(stats, context, this.response);
  93. }, config.timeout);
  94.  
  95. self
  96. .fetch(this.request)
  97. .then((response: Response): Promise<string | ArrayBuffer> => {
  98. this.response = this.loader = response;
  99.  
  100. if (!response.ok) {
  101. const { status, statusText } = response;
  102. throw new FetchError(
  103. statusText || 'fetch, bad network response',
  104. status,
  105. response
  106. );
  107. }
  108. stats.loading.first = Math.max(
  109. self.performance.now(),
  110. stats.loading.start
  111. );
  112. stats.total = parseInt(response.headers.get('Content-Length') || '0');
  113.  
  114. if (onProgress && Number.isFinite(config.highWaterMark)) {
  115. return this.loadProgressively(
  116. response,
  117. stats,
  118. context,
  119. config.highWaterMark,
  120. onProgress
  121. );
  122. }
  123.  
  124. if (isArrayBuffer) {
  125. return response.arrayBuffer();
  126. }
  127. return response.text();
  128. })
  129. .then((responseData: string | ArrayBuffer) => {
  130. const { response } = this;
  131. self.clearTimeout(this.requestTimeout);
  132. stats.loading.end = Math.max(
  133. self.performance.now(),
  134. stats.loading.first
  135. );
  136. stats.loaded = stats.total = responseData[LENGTH];
  137.  
  138. const loaderResponse = {
  139. url: response.url,
  140. data: responseData,
  141. };
  142.  
  143. if (onProgress && !Number.isFinite(config.highWaterMark)) {
  144. onProgress(stats, context, responseData, response);
  145. }
  146.  
  147. callbacks.onSuccess(loaderResponse, stats, context, response);
  148. })
  149. .catch((error) => {
  150. self.clearTimeout(this.requestTimeout);
  151. if (stats.aborted) {
  152. return;
  153. }
  154. // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior
  155. // when destroying, 'error' itself can be undefined
  156. const code: number = !error ? 0 : error.code || 0;
  157. const text: string = !error ? null : error.message;
  158. callbacks.onError(
  159. { code, text },
  160. context,
  161. error ? error.details : null
  162. );
  163. });
  164. }
  165.  
  166. getCacheAge(): number | null {
  167. let result: number | null = null;
  168. if (this.response) {
  169. const ageHeader = this.response.headers.get('age');
  170. result = ageHeader ? parseFloat(ageHeader) : null;
  171. }
  172. return result;
  173. }
  174.  
  175. private loadProgressively(
  176. response: Response,
  177. stats: LoaderStats,
  178. context: LoaderContext,
  179. highWaterMark: number = 0,
  180. onProgress: LoaderOnProgress<LoaderContext>
  181. ): Promise<ArrayBuffer> {
  182. const chunkCache = new ChunkCache();
  183. const reader = (response.body as ReadableStream).getReader();
  184.  
  185. const pump = (): Promise<ArrayBuffer> => {
  186. return reader
  187. .read()
  188. .then((data) => {
  189. if (data.done) {
  190. if (chunkCache.dataLength) {
  191. onProgress(stats, context, chunkCache.flush(), response);
  192. }
  193.  
  194. return Promise.resolve(new ArrayBuffer(0));
  195. }
  196. const chunk: Uint8Array = data.value;
  197. const len = chunk.length;
  198. stats.loaded += len;
  199. if (len < highWaterMark || chunkCache.dataLength) {
  200. // The current chunk is too small to to be emitted or the cache already has data
  201. // Push it to the cache
  202. chunkCache.push(chunk);
  203. if (chunkCache.dataLength >= highWaterMark) {
  204. // flush in order to join the typed arrays
  205. onProgress(stats, context, chunkCache.flush(), response);
  206. }
  207. } else {
  208. // If there's nothing cached already, and the chache is large enough
  209. // just emit the progress event
  210. onProgress(stats, context, chunk, response);
  211. }
  212. return pump();
  213. })
  214. .catch(() => {
  215. /* aborted */
  216. return Promise.reject();
  217. });
  218. };
  219.  
  220. return pump();
  221. }
  222. }
  223.  
  224. function getRequestParameters(context: LoaderContext, signal): any {
  225. const initParams: any = {
  226. method: 'GET',
  227. mode: 'cors',
  228. credentials: 'same-origin',
  229. signal,
  230. headers: new self.Headers(Object.assign({}, context.headers)),
  231. };
  232.  
  233. if (context.rangeEnd) {
  234. initParams.headers.set(
  235. 'Range',
  236. 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1)
  237. );
  238. }
  239.  
  240. return initParams;
  241. }
  242.  
  243. function getRequest(context: LoaderContext, initParams: any): Request {
  244. return new self.Request(context.url, initParams);
  245. }
  246.  
  247. class FetchError extends Error {
  248. public code: number;
  249. public details: any;
  250. constructor(message: string, code: number, details: any) {
  251. super(message);
  252. this.code = code;
  253. this.details = details;
  254. }
  255. }
  256.  
  257. export default FetchLoader;