src/utils/fetch-loader.ts
- import {
- LoaderCallbacks,
- LoaderContext,
- Loader,
- LoaderStats,
- LoaderConfiguration,
- LoaderOnProgress,
- } from '../types/loader';
- import { LoadStats } from '../loader/load-stats';
- import ChunkCache from '../demux/chunk-cache';
-
- export function fetchSupported() {
- if (
- self.fetch &&
- self.AbortController &&
- self.ReadableStream &&
- self.Request
- ) {
- try {
- new self.ReadableStream({}); // eslint-disable-line no-new
- return true;
- } catch (e) {
- /* noop */
- }
- }
- return false;
- }
-
- class FetchLoader implements Loader<LoaderContext> {
- private fetchSetup: Function;
- private requestTimeout?: number;
- private request!: Request;
- private response!: Response;
- private controller: AbortController;
- public context!: LoaderContext;
- private config: LoaderConfiguration | null = null;
- private callbacks: LoaderCallbacks<LoaderContext> | null = null;
- public stats: LoaderStats;
- public loader: Response | null = null;
-
- constructor(config /* HlsConfig */) {
- this.fetchSetup = config.fetchSetup || getRequest;
- this.controller = new self.AbortController();
- this.stats = new LoadStats();
- }
-
- destroy(): void {
- this.loader = this.callbacks = null;
- this.abortInternal();
- }
-
- abortInternal(): void {
- this.stats.aborted = true;
- this.controller.abort();
- }
-
- abort(): void {
- this.abortInternal();
- if (this.callbacks?.onAbort) {
- this.callbacks.onAbort(this.stats, this.context, this.response);
- }
- }
-
- load(
- context: LoaderContext,
- config: LoaderConfiguration,
- callbacks: LoaderCallbacks<LoaderContext>
- ): void {
- const stats = this.stats;
- if (stats.loading.start) {
- throw new Error('Loader can only be used once.');
- }
- stats.loading.start = self.performance.now();
-
- const initParams = getRequestParameters(context, this.controller.signal);
- const onProgress: LoaderOnProgress<LoaderContext> | undefined =
- callbacks.onProgress;
- const isArrayBuffer = context.responseType === 'arraybuffer';
- const LENGTH = isArrayBuffer ? 'byteLength' : 'length';
-
- this.context = context;
- this.config = config;
- this.callbacks = callbacks;
- this.request = this.fetchSetup(context, initParams);
- self.clearTimeout(this.requestTimeout);
- this.requestTimeout = self.setTimeout(() => {
- this.abortInternal();
- callbacks.onTimeout(stats, context, this.response);
- }, config.timeout);
-
- self
- .fetch(this.request)
- .then(
- (response: Response): Promise<string | ArrayBuffer> => {
- this.response = this.loader = response;
-
- if (!response.ok) {
- const { status, statusText } = response;
- throw new FetchError(
- statusText || 'fetch, bad network response',
- status,
- response
- );
- }
- stats.loading.first = Math.max(
- self.performance.now(),
- stats.loading.start
- );
- stats.total = parseInt(response.headers.get('Content-Length') || '0');
-
- if (onProgress && Number.isFinite(config.highWaterMark)) {
- this.loadProgressively(
- response,
- stats,
- context,
- config.highWaterMark,
- onProgress
- );
- }
-
- if (isArrayBuffer) {
- return response.arrayBuffer();
- }
- return response.text();
- }
- )
- .then((responseData: string | ArrayBuffer) => {
- const { response } = this;
- self.clearTimeout(this.requestTimeout);
- stats.loading.end = Math.max(
- self.performance.now(),
- stats.loading.first
- );
- stats.loaded = stats.total = responseData[LENGTH];
-
- const loaderResponse = {
- url: response.url,
- data: responseData,
- };
-
- if (onProgress && !Number.isFinite(config.highWaterMark)) {
- onProgress(stats, context, responseData, response);
- }
-
- callbacks.onSuccess(loaderResponse, stats, context, response);
- })
- .catch((error) => {
- self.clearTimeout(this.requestTimeout);
- if (stats.aborted) {
- return;
- }
- // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior
- const code = error.code || 0;
- callbacks.onError(
- { code, text: error.message },
- context,
- error.details
- );
- });
- }
-
- getResponseHeader(name: string): string | null {
- if (this.response) {
- try {
- return this.response.headers.get(name);
- } catch (error) {
- /* Could not get header */
- }
- }
- return null;
- }
-
- private loadProgressively(
- response: Response,
- stats: LoaderStats,
- context: LoaderContext,
- highWaterMark: number = 0,
- onProgress: LoaderOnProgress<LoaderContext>
- ) {
- const chunkCache = new ChunkCache();
- const reader = (response.clone().body as ReadableStream).getReader();
-
- const pump = () => {
- reader
- .read()
- .then((data: { done: boolean; value: Uint8Array }) => {
- if (data.done) {
- if (chunkCache.dataLength) {
- onProgress(stats, context, chunkCache.flush(), response);
- }
- return;
- }
- const chunk = data.value;
- const len = chunk.length;
- stats.loaded += len;
- if (len < highWaterMark || chunkCache.dataLength) {
- // The current chunk is too small to to be emitted or the cache already has data
- // Push it to the cache
- chunkCache.push(chunk);
- if (chunkCache.dataLength >= highWaterMark) {
- // flush in order to join the typed arrays
- onProgress(stats, context, chunkCache.flush(), response);
- }
- } else {
- // If there's nothing cached already, and the chache is large enough
- // just emit the progress event
- onProgress(stats, context, chunk, response);
- }
- pump();
- })
- .catch(() => {
- /* aborted */
- });
- };
-
- pump();
- }
- }
-
- function getRequestParameters(context: LoaderContext, signal): any {
- const initParams: any = {
- method: 'GET',
- mode: 'cors',
- credentials: 'same-origin',
- signal,
- };
-
- if (context.rangeEnd) {
- initParams.headers = new self.Headers({
- Range: 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1),
- });
- }
-
- return initParams;
- }
-
- function getRequest(context: LoaderContext, initParams: any): Request {
- return new self.Request(context.url, initParams);
- }
-
- class FetchError extends Error {
- public code: number;
- public details: any;
- constructor(message: string, code: number, details: any) {
- super(message);
- this.code = code;
- this.details = details;
- }
- }
-
- export default FetchLoader;