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