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, PartsLoadedData } 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: frag.sn === 'initSegment' ? Infinity : 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. let payload = response.data as ArrayBuffer;
  86. if (context.resetIV && frag.decryptdata) {
  87. frag.decryptdata.iv = new Uint8Array(payload.slice(0, 16));
  88. payload = payload.slice(16);
  89. }
  90. resolve({
  91. frag,
  92. part: null,
  93. payload,
  94. networkDetails,
  95. });
  96. },
  97. onError: (response, context, networkDetails) => {
  98. this.resetLoader(frag, loader);
  99. reject(
  100. new LoadError({
  101. type: ErrorTypes.NETWORK_ERROR,
  102. details: ErrorDetails.FRAG_LOAD_ERROR,
  103. fatal: false,
  104. frag,
  105. response,
  106. networkDetails,
  107. })
  108. );
  109. },
  110. onAbort: (stats, context, networkDetails) => {
  111. this.resetLoader(frag, loader);
  112. reject(
  113. new LoadError({
  114. type: ErrorTypes.NETWORK_ERROR,
  115. details: ErrorDetails.INTERNAL_ABORTED,
  116. fatal: false,
  117. frag,
  118. networkDetails,
  119. })
  120. );
  121. },
  122. onTimeout: (response, context, networkDetails) => {
  123. this.resetLoader(frag, loader);
  124. reject(
  125. new LoadError({
  126. type: ErrorTypes.NETWORK_ERROR,
  127. details: ErrorDetails.FRAG_LOAD_TIMEOUT,
  128. fatal: false,
  129. frag,
  130. networkDetails,
  131. })
  132. );
  133. },
  134. onProgress: (stats, context, data, networkDetails) => {
  135. if (onProgress) {
  136. onProgress({
  137. frag,
  138. part: null,
  139. payload: data as ArrayBuffer,
  140. networkDetails,
  141. });
  142. }
  143. },
  144. });
  145. });
  146. }
  147.  
  148. public loadPart(
  149. frag: Fragment,
  150. part: Part,
  151. onProgress: FragmentLoadProgressCallback
  152. ): Promise<FragLoadedData> {
  153. this.abort();
  154.  
  155. const config = this.config;
  156. const FragmentILoader = config.fLoader;
  157. const DefaultILoader = config.loader;
  158.  
  159. return new Promise((resolve, reject) => {
  160. if (this.loader) {
  161. this.loader.destroy();
  162. }
  163. const loader =
  164. (this.loader =
  165. frag.loader =
  166. FragmentILoader
  167. ? new FragmentILoader(config)
  168. : (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
  169. const loaderContext = createLoaderContext(frag, part);
  170. const loaderConfig: LoaderConfiguration = {
  171. timeout: config.fragLoadingTimeOut,
  172. maxRetry: 0,
  173. retryDelay: 0,
  174. maxRetryDelay: config.fragLoadingMaxRetryTimeout,
  175. highWaterMark: MIN_CHUNK_SIZE,
  176. };
  177. // Assign part stats to the loader's stats reference
  178. part.stats = loader.stats;
  179. loader.load(loaderContext, loaderConfig, {
  180. onSuccess: (response, stats, context, networkDetails) => {
  181. this.resetLoader(frag, loader);
  182. this.updateStatsFromPart(frag, part);
  183. const partLoadedData: FragLoadedData = {
  184. frag,
  185. part,
  186. payload: response.data as ArrayBuffer,
  187. networkDetails,
  188. };
  189. onProgress(partLoadedData);
  190. resolve(partLoadedData);
  191. },
  192. onError: (response, context, networkDetails) => {
  193. this.resetLoader(frag, loader);
  194. reject(
  195. new LoadError({
  196. type: ErrorTypes.NETWORK_ERROR,
  197. details: ErrorDetails.FRAG_LOAD_ERROR,
  198. fatal: false,
  199. frag,
  200. part,
  201. response,
  202. networkDetails,
  203. })
  204. );
  205. },
  206. onAbort: (stats, context, networkDetails) => {
  207. frag.stats.aborted = part.stats.aborted;
  208. this.resetLoader(frag, loader);
  209. reject(
  210. new LoadError({
  211. type: ErrorTypes.NETWORK_ERROR,
  212. details: ErrorDetails.INTERNAL_ABORTED,
  213. fatal: false,
  214. frag,
  215. part,
  216. networkDetails,
  217. })
  218. );
  219. },
  220. onTimeout: (response, context, networkDetails) => {
  221. this.resetLoader(frag, loader);
  222. reject(
  223. new LoadError({
  224. type: ErrorTypes.NETWORK_ERROR,
  225. details: ErrorDetails.FRAG_LOAD_TIMEOUT,
  226. fatal: false,
  227. frag,
  228. part,
  229. networkDetails,
  230. })
  231. );
  232. },
  233. });
  234. });
  235. }
  236.  
  237. private updateStatsFromPart(frag: Fragment, part: Part) {
  238. const fragStats = frag.stats;
  239. const partStats = part.stats;
  240. const partTotal = partStats.total;
  241. fragStats.loaded += partStats.loaded;
  242. if (partTotal) {
  243. const estTotalParts = Math.round(frag.duration / part.duration);
  244. const estLoadedParts = Math.min(
  245. Math.round(fragStats.loaded / partTotal),
  246. estTotalParts
  247. );
  248. const estRemainingParts = estTotalParts - estLoadedParts;
  249. const estRemainingBytes =
  250. estRemainingParts * Math.round(fragStats.loaded / estLoadedParts);
  251. fragStats.total = fragStats.loaded + estRemainingBytes;
  252. } else {
  253. fragStats.total = Math.max(fragStats.loaded, fragStats.total);
  254. }
  255. const fragLoading = fragStats.loading;
  256. const partLoading = partStats.loading;
  257. if (fragLoading.start) {
  258. // add to fragment loader latency
  259. fragLoading.first += partLoading.first - partLoading.start;
  260. } else {
  261. fragLoading.start = partLoading.start;
  262. fragLoading.first = partLoading.first;
  263. }
  264. fragLoading.end = partLoading.end;
  265. }
  266.  
  267. private resetLoader(frag: Fragment, loader: Loader<FragmentLoaderContext>) {
  268. frag.loader = null;
  269. if (this.loader === loader) {
  270. self.clearTimeout(this.partLoadTimeout);
  271. this.loader = null;
  272. }
  273. loader.destroy();
  274. }
  275. }
  276.  
  277. function createLoaderContext(
  278. frag: Fragment,
  279. part: Part | null = null
  280. ): FragmentLoaderContext {
  281. const segment: BaseSegment = part || frag;
  282. const loaderContext: FragmentLoaderContext = {
  283. frag,
  284. part,
  285. responseType: 'arraybuffer',
  286. url: segment.url,
  287. headers: {},
  288. rangeStart: 0,
  289. rangeEnd: 0,
  290. };
  291. const start = segment.byteRangeStartOffset;
  292. const end = segment.byteRangeEndOffset;
  293. if (Number.isFinite(start) && Number.isFinite(end)) {
  294. let byteRangeStart = start;
  295. let byteRangeEnd = end;
  296. if (frag.sn === 'initSegment' && frag.decryptdata?.method === 'AES-128') {
  297. // MAP segment encrypted with method 'AES-128', when served with HTTP Range,
  298. // has the unencrypted size specified in the range.
  299. // Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
  300. const fragmentLen = end - start;
  301. if (fragmentLen % 16) {
  302. byteRangeEnd = end + (16 - (fragmentLen % 16));
  303. }
  304. if (start !== 0) {
  305. loaderContext.resetIV = true;
  306. byteRangeStart = start - 16;
  307. }
  308. }
  309. loaderContext.rangeStart = byteRangeStart;
  310. loaderContext.rangeEnd = byteRangeEnd;
  311. }
  312. return loaderContext;
  313. }
  314.  
  315. export class LoadError extends Error {
  316. public readonly data: FragLoadFailResult;
  317. constructor(data: FragLoadFailResult, ...params) {
  318. super(...params);
  319. this.data = data;
  320. }
  321. }
  322.  
  323. export interface FragLoadFailResult {
  324. type: string;
  325. details: string;
  326. fatal: boolean;
  327. frag: Fragment;
  328. part?: Part;
  329. response?: {
  330. // error status code
  331. code: number;
  332. // error description
  333. text: string;
  334. };
  335. networkDetails: any;
  336. }
  337.  
  338. export type FragmentLoadProgressCallback = (
  339. result: FragLoadedData | PartsLoadedData
  340. ) => void;