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