src/loader/fragment-loader.ts
import { ErrorTypes, ErrorDetails } from '../errors';
import Fragment from './fragment';
import {
Loader,
LoaderConfiguration,
FragmentLoaderContext
} from '../types/loader';
import type { HlsConfig } from '../config';
import type { BaseSegment, Part } from './fragment';
import type { FragLoadedData } from '../types/events';
const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb
export default class FragmentLoader {
private readonly config: HlsConfig;
private loader: Loader<FragmentLoaderContext> | null = null;
private partLoadTimeout: number = -1;
constructor (config: HlsConfig) {
this.config = config;
}
abort () {
if (this.loader) {
// Abort the loader for current fragment. Only one may load at any given time
this.loader.abort();
}
}
load (frag: Fragment, onProgress?: FragmentLoadProgressCallback): Promise<FragLoadedData> {
const url = frag.url;
if (!url) {
return Promise.reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
networkDetails: null
}, `Fragment does not have a ${url ? 'part list' : 'url'}`));
}
this.abort();
const config = this.config;
const FragmentILoader = config.fLoader;
const DefaultILoader = config.loader;
return new Promise((resolve, reject) => {
const loader = this.loader = frag.loader =
FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config) as Loader<FragmentLoaderContext>;
const loaderContext = createLoaderContext(frag);
const loaderConfig: LoaderConfiguration = {
timeout: config.fragLoadingTimeOut,
maxRetry: 0,
retryDelay: 0,
maxRetryDelay: config.fragLoadingMaxRetryTimeout,
highWaterMark: MIN_CHUNK_SIZE
};
// Assign frag stats to the loader's stats reference
frag.stats = loader.stats;
loader.load(loaderContext, loaderConfig, {
onSuccess: (response, stats, context, networkDetails) => {
this.resetLoader(frag, loader);
resolve({
frag,
part: null,
payload: response.data as ArrayBuffer,
networkDetails
});
},
onError: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
response,
networkDetails
}));
},
onAbort: (stats, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.INTERNAL_ABORTED,
fatal: false,
frag,
networkDetails
}));
},
onTimeout: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
fatal: false,
frag,
networkDetails
}));
},
onProgress: (stats, context, data, networkDetails) => {
if (onProgress) {
onProgress({
frag,
part: null,
payload: data as ArrayBuffer,
networkDetails
});
}
}
});
});
}
public loadPart (frag: Fragment, part: Part, onProgress: FragmentLoadProgressCallback): Promise<FragLoadedData> {
this.abort();
const config = this.config;
const FragmentILoader = config.fLoader;
const DefaultILoader = config.loader;
return new Promise((resolve, reject) => {
const loader = this.loader = frag.loader =
FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config) as Loader<FragmentLoaderContext>;
const loaderContext = createLoaderContext(frag, part);
const loaderConfig: LoaderConfiguration = {
timeout: config.fragLoadingTimeOut,
maxRetry: 0,
retryDelay: 0,
maxRetryDelay: config.fragLoadingMaxRetryTimeout,
highWaterMark: MIN_CHUNK_SIZE
};
// Assign part stats to the loader's stats reference
part.stats = loader.stats;
loader.load(loaderContext, loaderConfig, {
onSuccess: (response, stats, context, networkDetails) => {
this.resetLoader(frag, loader);
this.updateStatsFromPart(frag, part);
const partLoadedData: FragLoadedData = {
frag,
part,
payload: response.data as ArrayBuffer,
networkDetails
};
onProgress(partLoadedData);
resolve(partLoadedData);
},
onError: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
part,
response,
networkDetails
}));
},
onAbort: (stats, context, networkDetails) => {
frag.stats.aborted = part.stats.aborted;
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.INTERNAL_ABORTED,
fatal: false,
frag,
part,
networkDetails
}));
},
onTimeout: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
fatal: false,
frag,
part,
networkDetails
}));
}
});
});
}
private updateStatsFromPart (frag: Fragment, part: Part) {
const fragStats = frag.stats;
const partStats = part.stats;
const partTotal = partStats.total;
fragStats.loaded += partStats.loaded;
if (partTotal) {
const estTotalParts = Math.round(frag.duration / part.duration);
const estLoadedParts = Math.min(Math.round(fragStats.loaded / partTotal), estTotalParts);
const estRemainingParts = estTotalParts - estLoadedParts;
const estRemainingBytes = estRemainingParts * Math.round(fragStats.loaded / estLoadedParts);
fragStats.total = fragStats.loaded + estRemainingBytes;
} else {
fragStats.total = Math.max(fragStats.loaded, fragStats.total);
}
const fragLoading = fragStats.loading;
const partLoading = partStats.loading;
if (fragLoading.start) {
// add to fragment loader latency
fragLoading.first += partLoading.first - partLoading.start;
} else {
fragLoading.start = partLoading.start;
fragLoading.first = partLoading.first;
}
fragLoading.end = partLoading.end;
}
private resetLoader (frag: Fragment, loader: Loader<FragmentLoaderContext>) {
frag.loader = null;
if (this.loader === loader) {
self.clearTimeout(this.partLoadTimeout);
this.loader = null;
}
}
}
function createLoaderContext (frag: Fragment, part: Part | null = null): FragmentLoaderContext {
const segment: BaseSegment = part || frag;
const loaderContext: FragmentLoaderContext = {
frag,
part,
responseType: 'arraybuffer',
url: segment.url,
rangeStart: 0,
rangeEnd: 0
};
const start = segment.byteRangeStartOffset;
const end = segment.byteRangeEndOffset;
if (Number.isFinite(start) && Number.isFinite(end)) {
loaderContext.rangeStart = start;
loaderContext.rangeEnd = end;
}
return loaderContext;
}
export class LoadError extends Error {
public readonly data: FragLoadFailResult;
constructor (data: FragLoadFailResult, ...params) {
super(...params);
this.data = data;
}
}
export interface FragLoadFailResult {
type: string
details: string
fatal: boolean
frag: Fragment
part?: Part
response?: {
// error status code
code: number,
// error description
text: string,
}
networkDetails: any
}
export type FragmentLoadProgressCallback = (result: FragLoadedData) => void;