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. const total = responseData[LENGTH];
  137. if (total) {
  138. stats.loaded = stats.total = total;
  139. }
  140.  
  141. const loaderResponse = {
  142. url: response.url,
  143. data: responseData,
  144. };
  145.  
  146. if (onProgress && !Number.isFinite(config.highWaterMark)) {
  147. onProgress(stats, context, responseData, response);
  148. }
  149.  
  150. callbacks.onSuccess(loaderResponse, stats, context, response);
  151. })
  152. .catch((error) => {
  153. self.clearTimeout(this.requestTimeout);
  154. if (stats.aborted) {
  155. return;
  156. }
  157. // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior
  158. // when destroying, 'error' itself can be undefined
  159. const code: number = !error ? 0 : error.code || 0;
  160. const text: string = !error ? null : error.message;
  161. callbacks.onError(
  162. { code, text },
  163. context,
  164. error ? error.details : null
  165. );
  166. });
  167. }
  168.  
  169. getCacheAge(): number | null {
  170. let result: number | null = null;
  171. if (this.response) {
  172. const ageHeader = this.response.headers.get('age');
  173. result = ageHeader ? parseFloat(ageHeader) : null;
  174. }
  175. return result;
  176. }
  177.  
  178. private loadProgressively(
  179. response: Response,
  180. stats: LoaderStats,
  181. context: LoaderContext,
  182. highWaterMark: number = 0,
  183. onProgress: LoaderOnProgress<LoaderContext>
  184. ): Promise<ArrayBuffer> {
  185. const chunkCache = new ChunkCache();
  186. const reader = (response.body as ReadableStream).getReader();
  187.  
  188. const pump = (): Promise<ArrayBuffer> => {
  189. return reader
  190. .read()
  191. .then((data) => {
  192. if (data.done) {
  193. if (chunkCache.dataLength) {
  194. onProgress(stats, context, chunkCache.flush(), response);
  195. }
  196.  
  197. return Promise.resolve(new ArrayBuffer(0));
  198. }
  199. const chunk: Uint8Array = data.value;
  200. const len = chunk.length;
  201. stats.loaded += len;
  202. if (len < highWaterMark || chunkCache.dataLength) {
  203. // The current chunk is too small to to be emitted or the cache already has data
  204. // Push it to the cache
  205. chunkCache.push(chunk);
  206. if (chunkCache.dataLength >= highWaterMark) {
  207. // flush in order to join the typed arrays
  208. onProgress(stats, context, chunkCache.flush(), response);
  209. }
  210. } else {
  211. // If there's nothing cached already, and the chache is large enough
  212. // just emit the progress event
  213. onProgress(stats, context, chunk, response);
  214. }
  215. return pump();
  216. })
  217. .catch(() => {
  218. /* aborted */
  219. return Promise.reject();
  220. });
  221. };
  222.  
  223. return pump();
  224. }
  225. }
  226.  
  227. function getRequestParameters(context: LoaderContext, signal): any {
  228. const initParams: any = {
  229. method: 'GET',
  230. mode: 'cors',
  231. credentials: 'same-origin',
  232. signal,
  233. headers: new self.Headers(Object.assign({}, context.headers)),
  234. };
  235.  
  236. if (context.rangeEnd) {
  237. initParams.headers.set(
  238. 'Range',
  239. 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1)
  240. );
  241. }
  242.  
  243. return initParams;
  244. }
  245.  
  246. function getRequest(context: LoaderContext, initParams: any): Request {
  247. return new self.Request(context.url, initParams);
  248. }
  249.  
  250. class FetchError extends Error {
  251. public code: number;
  252. public details: any;
  253. constructor(message: string, code: number, details: any) {
  254. super(message);
  255. this.code = code;
  256. this.details = details;
  257. }
  258. }
  259.  
  260. export default FetchLoader;