Home Reference Source

src/loader/fragment-loader.ts

  1. import { ErrorTypes, ErrorDetails } from '../errors';
  2. import Fragment from './fragment';
  3. import {
  4. Loader,
  5. LoaderConfiguration,
  6. FragmentLoaderContext
  7. } from '../types/loader';
  8. import type { HlsConfig } from '../config';
  9. import type { BaseSegment, Part } from './fragment';
  10. import type { FragLoadedData } from '../types/events';
  11.  
  12. const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb
  13.  
  14. export default class FragmentLoader {
  15. private readonly config: HlsConfig;
  16. private loader: Loader<FragmentLoaderContext> | null = null;
  17. private partLoadTimeout: number = -1;
  18.  
  19. constructor (config: HlsConfig) {
  20. this.config = config;
  21. }
  22.  
  23. abort () {
  24. if (this.loader) {
  25. // Abort the loader for current fragment. Only one may load at any given time
  26. this.loader.abort();
  27. }
  28. }
  29.  
  30. load (frag: Fragment, onProgress?: FragmentLoadProgressCallback): Promise<FragLoadedData> {
  31. const url = frag.url;
  32. if (!url) {
  33. return Promise.reject(new LoadError({
  34. type: ErrorTypes.NETWORK_ERROR,
  35. details: ErrorDetails.FRAG_LOAD_ERROR,
  36. fatal: false,
  37. frag,
  38. networkDetails: null
  39. }, `Fragment does not have a ${url ? 'part list' : 'url'}`));
  40. }
  41. this.abort();
  42.  
  43. const config = this.config;
  44. const FragmentILoader = config.fLoader;
  45. const DefaultILoader = config.loader;
  46.  
  47. return new Promise((resolve, reject) => {
  48. const loader = this.loader = frag.loader =
  49. FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config) as Loader<FragmentLoaderContext>;
  50. const loaderContext = createLoaderContext(frag);
  51. const loaderConfig: LoaderConfiguration = {
  52. timeout: config.fragLoadingTimeOut,
  53. maxRetry: 0,
  54. retryDelay: 0,
  55. maxRetryDelay: config.fragLoadingMaxRetryTimeout,
  56. highWaterMark: MIN_CHUNK_SIZE
  57. };
  58. // Assign frag stats to the loader's stats reference
  59. frag.stats = loader.stats;
  60. loader.load(loaderContext, loaderConfig, {
  61. onSuccess: (response, stats, context, networkDetails) => {
  62. this.resetLoader(frag, loader);
  63. resolve({
  64. frag,
  65. part: null,
  66. payload: response.data as ArrayBuffer,
  67. networkDetails
  68. });
  69. },
  70. onError: (response, context, networkDetails) => {
  71. this.resetLoader(frag, loader);
  72. reject(new LoadError({
  73. type: ErrorTypes.NETWORK_ERROR,
  74. details: ErrorDetails.FRAG_LOAD_ERROR,
  75. fatal: false,
  76. frag,
  77. response,
  78. networkDetails
  79. }));
  80. },
  81. onAbort: (stats, context, networkDetails) => {
  82. this.resetLoader(frag, loader);
  83. reject(new LoadError({
  84. type: ErrorTypes.NETWORK_ERROR,
  85. details: ErrorDetails.INTERNAL_ABORTED,
  86. fatal: false,
  87. frag,
  88. networkDetails
  89. }));
  90. },
  91. onTimeout: (response, context, networkDetails) => {
  92. this.resetLoader(frag, loader);
  93. reject(new LoadError({
  94. type: ErrorTypes.NETWORK_ERROR,
  95. details: ErrorDetails.FRAG_LOAD_TIMEOUT,
  96. fatal: false,
  97. frag,
  98. networkDetails
  99. }));
  100. },
  101. onProgress: (stats, context, data, networkDetails) => {
  102. if (onProgress) {
  103. onProgress({
  104. frag,
  105. part: null,
  106. payload: data as ArrayBuffer,
  107. networkDetails
  108. });
  109. }
  110. }
  111. });
  112. });
  113. }
  114.  
  115. public loadPart (frag: Fragment, part: Part, onProgress: FragmentLoadProgressCallback): Promise<FragLoadedData> {
  116. this.abort();
  117.  
  118. const config = this.config;
  119. const FragmentILoader = config.fLoader;
  120. const DefaultILoader = config.loader;
  121.  
  122. return new Promise((resolve, reject) => {
  123. const loader = this.loader = frag.loader =
  124. FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config) as Loader<FragmentLoaderContext>;
  125. const loaderContext = createLoaderContext(frag, part);
  126. const loaderConfig: LoaderConfiguration = {
  127. timeout: config.fragLoadingTimeOut,
  128. maxRetry: 0,
  129. retryDelay: 0,
  130. maxRetryDelay: config.fragLoadingMaxRetryTimeout,
  131. highWaterMark: MIN_CHUNK_SIZE
  132. };
  133. // Assign part stats to the loader's stats reference
  134. part.stats = loader.stats;
  135. loader.load(loaderContext, loaderConfig, {
  136. onSuccess: (response, stats, context, networkDetails) => {
  137. this.resetLoader(frag, loader);
  138. this.updateStatsFromPart(frag, part);
  139. const partLoadedData: FragLoadedData = {
  140. frag,
  141. part,
  142. payload: response.data as ArrayBuffer,
  143. networkDetails
  144. };
  145. onProgress(partLoadedData);
  146. resolve(partLoadedData);
  147. },
  148. onError: (response, context, networkDetails) => {
  149. this.resetLoader(frag, loader);
  150. reject(new LoadError({
  151. type: ErrorTypes.NETWORK_ERROR,
  152. details: ErrorDetails.FRAG_LOAD_ERROR,
  153. fatal: false,
  154. frag,
  155. part,
  156. response,
  157. networkDetails
  158. }));
  159. },
  160. onAbort: (stats, context, networkDetails) => {
  161. frag.stats.aborted = part.stats.aborted;
  162. this.resetLoader(frag, loader);
  163. reject(new LoadError({
  164. type: ErrorTypes.NETWORK_ERROR,
  165. details: ErrorDetails.INTERNAL_ABORTED,
  166. fatal: false,
  167. frag,
  168. part,
  169. networkDetails
  170. }));
  171. },
  172. onTimeout: (response, context, networkDetails) => {
  173. this.resetLoader(frag, loader);
  174. reject(new LoadError({
  175. type: ErrorTypes.NETWORK_ERROR,
  176. details: ErrorDetails.FRAG_LOAD_TIMEOUT,
  177. fatal: false,
  178. frag,
  179. part,
  180. networkDetails
  181. }));
  182. }
  183. });
  184. });
  185. }
  186.  
  187. private updateStatsFromPart (frag: Fragment, part: Part) {
  188. const fragStats = frag.stats;
  189. const partStats = part.stats;
  190. const partTotal = partStats.total;
  191. fragStats.loaded += partStats.loaded;
  192. if (partTotal) {
  193. const estTotalParts = Math.round(frag.duration / part.duration);
  194. const estLoadedParts = Math.min(Math.round(fragStats.loaded / partTotal), estTotalParts);
  195. const estRemainingParts = estTotalParts - estLoadedParts;
  196. const estRemainingBytes = estRemainingParts * Math.round(fragStats.loaded / estLoadedParts);
  197. fragStats.total = fragStats.loaded + estRemainingBytes;
  198. } else {
  199. fragStats.total = Math.max(fragStats.loaded, fragStats.total);
  200. }
  201. const fragLoading = fragStats.loading;
  202. const partLoading = partStats.loading;
  203. if (fragLoading.start) {
  204. // add to fragment loader latency
  205. fragLoading.first += partLoading.first - partLoading.start;
  206. } else {
  207. fragLoading.start = partLoading.start;
  208. fragLoading.first = partLoading.first;
  209. }
  210. fragLoading.end = partLoading.end;
  211. }
  212.  
  213. private resetLoader (frag: Fragment, loader: Loader<FragmentLoaderContext>) {
  214. frag.loader = null;
  215. if (this.loader === loader) {
  216. self.clearTimeout(this.partLoadTimeout);
  217. this.loader = null;
  218. }
  219. }
  220. }
  221.  
  222. function createLoaderContext (frag: Fragment, part: Part | null = null): FragmentLoaderContext {
  223. const segment: BaseSegment = part || frag;
  224. const loaderContext: FragmentLoaderContext = {
  225. frag,
  226. part,
  227. responseType: 'arraybuffer',
  228. url: segment.url,
  229. rangeStart: 0,
  230. rangeEnd: 0
  231. };
  232. const start = segment.byteRangeStartOffset;
  233. const end = segment.byteRangeEndOffset;
  234. if (Number.isFinite(start) && Number.isFinite(end)) {
  235. loaderContext.rangeStart = start;
  236. loaderContext.rangeEnd = end;
  237. }
  238. return loaderContext;
  239. }
  240.  
  241. export class LoadError extends Error {
  242. public readonly data: FragLoadFailResult;
  243. constructor (data: FragLoadFailResult, ...params) {
  244. super(...params);
  245. this.data = data;
  246. }
  247. }
  248.  
  249. export interface FragLoadFailResult {
  250. type: string
  251. details: string
  252. fatal: boolean
  253. frag: Fragment
  254. part?: Part
  255. response?: {
  256. // error status code
  257. code: number,
  258. // error description
  259. text: string,
  260. }
  261. networkDetails: any
  262. }
  263.  
  264. export type FragmentLoadProgressCallback = (result: FragLoadedData) => void;