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