download
raw
9.61 kB
import ChunkCache from '../demux/chunk-cache';
import { isPromise } from '../demux/transmuxer';
import { LoadStats } from '../loader/load-stats';
import type { HlsConfig } from '../config';
import type {
Loader,
LoaderCallbacks,
LoaderConfiguration,
LoaderContext,
LoaderOnProgress,
LoaderResponse,
LoaderStats,
} from '../types/loader';
export function fetchSupported() {
if (
// @ts-ignore
self.fetch &&
self.AbortController &&
self.ReadableStream &&
self.Request
) {
try {
new self.ReadableStream({}); // eslint-disable-line no-new
return true;
} catch (e) {
/* noop */
}
}
return false;
}
const BYTERANGE = /(\d+)-(\d+)\/(\d+)/;
class FetchLoader implements Loader<LoaderContext> {
private fetchSetup: NonNullable<HlsConfig['fetchSetup']>;
private requestTimeout?: number;
private request: Promise<Request> | Request | null = null;
private response: Response | null = null;
private controller: AbortController;
public context: LoaderContext | null = null;
private config: LoaderConfiguration | null = null;
private callbacks: LoaderCallbacks<LoaderContext> | null = null;
public stats: LoaderStats;
private 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 =
this.context =
this.config =
this.request =
null;
this.abortInternal();
this.response = null;
// @ts-ignore
this.fetchSetup = this.controller = this.stats = null;
}
abortInternal(): void {
if (this.controller && !this.stats.loading.end) {
this.stats.aborted = true;
this.controller.abort();
}
}
abort(): void {
this.abortInternal();
if (this.callbacks?.onAbort) {
this.callbacks.onAbort(
this.stats,
this.context as LoaderContext,
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 isArrayBuffer = context.responseType === 'arraybuffer';
const LENGTH = isArrayBuffer ? 'byteLength' : 'length';
const { maxTimeToFirstByteMs, maxLoadTimeMs } = config.loadPolicy;
this.context = context;
this.config = config;
this.callbacks = callbacks;
this.request = this.fetchSetup(context, initParams);
self.clearTimeout(this.requestTimeout);
config.timeout =
maxTimeToFirstByteMs && Number.isFinite(maxTimeToFirstByteMs)
? maxTimeToFirstByteMs
: maxLoadTimeMs;
this.requestTimeout = self.setTimeout(() => {
if (this.callbacks) {
this.abortInternal();
this.callbacks.onTimeout(stats, context, this.response);
}
}, config.timeout);
const fetchPromise = isPromise(this.request)
? this.request.then(self.fetch)
: self.fetch(this.request);
fetchPromise
.then((response: Response): Promise<string | ArrayBuffer> => {
this.response = this.loader = response;
const first = Math.max(self.performance.now(), stats.loading.start);
self.clearTimeout(this.requestTimeout);
config.timeout = maxLoadTimeMs;
this.requestTimeout = self.setTimeout(
() => {
if (this.callbacks) {
this.abortInternal();
this.callbacks.onTimeout(stats, context, this.response);
}
},
maxLoadTimeMs - (first - stats.loading.start),
);
if (!response.ok) {
const { status, statusText } = response;
throw new FetchError(
statusText || 'fetch, bad network response',
status,
response,
);
}
stats.loading.first = first;
stats.total = getContentLength(response.headers) || stats.total;
const onProgress = this.callbacks?.onProgress;
if (onProgress && Number.isFinite(config.highWaterMark)) {
return this.loadProgressively(
response,
stats,
context,
config.highWaterMark,
onProgress,
);
}
if (isArrayBuffer) {
return response.arrayBuffer();
}
if (context.responseType === 'json') {
return response.json();
}
return response.text();
})
.then((responseData: string | ArrayBuffer) => {
const response = this.response;
if (!response) {
throw new Error('loader destroyed');
}
self.clearTimeout(this.requestTimeout);
stats.loading.end = Math.max(
self.performance.now(),
stats.loading.first,
);
const total = responseData[LENGTH];
if (total) {
stats.loaded = stats.total = total;
}
const loaderResponse: LoaderResponse = {
url: response.url,
data: responseData,
code: response.status,
};
const onProgress = this.callbacks?.onProgress;
if (onProgress && !Number.isFinite(config.highWaterMark)) {
onProgress(stats, context, responseData, response);
}
this.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
// when destroying, 'error' itself can be undefined
const code: number = !error ? 0 : error.code || 0;
const text: string = !error ? null : error.message;
this.callbacks?.onError(
{ code, text },
context,
error ? error.details : null,
stats,
);
});
}
getCacheAge(): number | null {
let result: number | null = null;
if (this.response) {
const ageHeader = this.response.headers.get('age');
result = ageHeader ? parseFloat(ageHeader) : null;
}
return result;
}
getResponseHeader(name: string): string | null {
return this.response ? this.response.headers.get(name) : null;
}
private loadProgressively(
response: Response,
stats: LoaderStats,
context: LoaderContext,
highWaterMark: number = 0,
onProgress: LoaderOnProgress<LoaderContext>,
): Promise<ArrayBuffer> {
const chunkCache = new ChunkCache();
const reader = (response.body as ReadableStream).getReader();
const pump = (): Promise<ArrayBuffer> => {
return reader
.read()
.then((data) => {
if (data.done) {
if (chunkCache.dataLength) {
onProgress(stats, context, chunkCache.flush().buffer, response);
}
return Promise.resolve(new ArrayBuffer(0));
}
const chunk: Uint8Array<ArrayBuffer> = 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().buffer, response);
}
} else {
// If there's nothing cached already, and the chache is large enough
// just emit the progress event
onProgress(stats, context, chunk.buffer, response);
}
return pump();
})
.catch(() => {
/* aborted */
return Promise.reject();
});
};
return pump();
}
}
function getRequestParameters(context: LoaderContext, signal): any {
const initParams: any = {
method: 'GET',
mode: 'cors',
credentials: 'same-origin',
signal,
headers: new self.Headers(Object.assign({}, context.headers)),
};
if (context.rangeEnd) {
initParams.headers.set(
'Range',
'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1),
);
}
return initParams;
}
function getByteRangeLength(byteRangeHeader: string): number | undefined {
const result = BYTERANGE.exec(byteRangeHeader);
if (result) {
return parseInt(result[2]) - parseInt(result[1]) + 1;
}
}
function getContentLength(headers: Headers): number | undefined {
const contentRange = headers.get('Content-Range');
if (contentRange) {
const byteRangeLength = getByteRangeLength(contentRange);
if (Number.isFinite(byteRangeLength)) {
return byteRangeLength;
}
}
const contentLength = headers.get('Content-Length');
if (contentLength) {
return parseInt(contentLength);
}
}
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;

Xet Storage Details

Size:
9.61 kB
·
Xet hash:
d97ffb0e499ec8237c091fe5230a6dfd609407553dac25450d8b4276f59498d8

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.