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;