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