Home Reference Source

src/controller/fragment-tracker.ts

  1. import { Events } from '../events';
  2. import { Fragment, Part } from '../loader/fragment';
  3. import { PlaylistLevelType } from '../types/loader';
  4. import type { SourceBufferName } from '../types/buffer';
  5. import type {
  6. FragmentBufferedRange,
  7. FragmentEntity,
  8. FragmentTimeRange,
  9. } from '../types/fragment-tracker';
  10. import type { ComponentAPI } from '../types/component-api';
  11. import type {
  12. BufferAppendedData,
  13. FragBufferedData,
  14. FragLoadedData,
  15. } from '../types/events';
  16. import type Hls from '../hls';
  17.  
  18. export enum FragmentState {
  19. NOT_LOADED = 'NOT_LOADED',
  20. APPENDING = 'APPENDING',
  21. PARTIAL = 'PARTIAL',
  22. OK = 'OK',
  23. }
  24.  
  25. export class FragmentTracker implements ComponentAPI {
  26. private activeFragment: Fragment | null = null;
  27. private activeParts: Part[] | null = null;
  28. private endListFragments: { [key in PlaylistLevelType]?: FragmentEntity } =
  29. Object.create(null);
  30. private fragments: Partial<Record<string, FragmentEntity>> =
  31. Object.create(null);
  32. private timeRanges:
  33. | {
  34. [key in SourceBufferName]?: TimeRanges;
  35. }
  36. | null = Object.create(null);
  37.  
  38. private bufferPadding: number = 0.2;
  39. private hls: Hls;
  40.  
  41. constructor(hls: Hls) {
  42. this.hls = hls;
  43.  
  44. this._registerListeners();
  45. }
  46.  
  47. private _registerListeners() {
  48. const { hls } = this;
  49. hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
  50. hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
  51. hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
  52. }
  53.  
  54. private _unregisterListeners() {
  55. const { hls } = this;
  56. hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
  57. hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
  58. hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
  59. }
  60.  
  61. public destroy() {
  62. this._unregisterListeners();
  63. // @ts-ignore
  64. this.fragments =
  65. // @ts-ignore
  66. this.endListFragments =
  67. this.timeRanges =
  68. this.activeFragment =
  69. this.activeParts =
  70. null;
  71. }
  72.  
  73. /**
  74. * Return a Fragment with an appended range that matches the position and levelType.
  75. * If not found any Fragment, return null
  76. */
  77. public getAppendedFrag(
  78. position: number,
  79. levelType: PlaylistLevelType
  80. ): Fragment | Part | null {
  81. if (levelType === PlaylistLevelType.MAIN) {
  82. const { activeFragment, activeParts } = this;
  83. if (!activeFragment) {
  84. return null;
  85. }
  86. if (activeParts) {
  87. for (let i = activeParts.length; i--; ) {
  88. const activePart = activeParts[i];
  89. const appendedPTS = activePart
  90. ? activePart.end
  91. : activeFragment.appendedPTS;
  92. if (
  93. activePart.start <= position &&
  94. appendedPTS !== undefined &&
  95. position <= appendedPTS
  96. ) {
  97. // 9 is a magic number. remove parts from lookup after a match but keep some short seeks back.
  98. if (i > 9) {
  99. this.activeParts = activeParts.slice(i - 9);
  100. }
  101. return activePart;
  102. }
  103. }
  104. } else if (
  105. activeFragment.start <= position &&
  106. activeFragment.appendedPTS !== undefined &&
  107. position <= activeFragment.appendedPTS
  108. ) {
  109. return activeFragment;
  110. }
  111. }
  112. return this.getBufferedFrag(position, levelType);
  113. }
  114.  
  115. /**
  116. * Return a buffered Fragment that matches the position and levelType.
  117. * A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted).
  118. * If not found any Fragment, return null
  119. */
  120. public getBufferedFrag(
  121. position: number,
  122. levelType: PlaylistLevelType
  123. ): Fragment | null {
  124. const { fragments } = this;
  125. const keys = Object.keys(fragments);
  126. for (let i = keys.length; i--; ) {
  127. const fragmentEntity = fragments[keys[i]];
  128. if (fragmentEntity?.body.type === levelType && fragmentEntity.buffered) {
  129. const frag = fragmentEntity.body;
  130. if (frag.start <= position && position <= frag.end) {
  131. return frag;
  132. }
  133. }
  134. }
  135. return null;
  136. }
  137.  
  138. /**
  139. * Partial fragments effected by coded frame eviction will be removed
  140. * The browser will unload parts of the buffer to free up memory for new buffer data
  141. * Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable)
  142. */
  143. public detectEvictedFragments(
  144. elementaryStream: SourceBufferName,
  145. timeRange: TimeRanges,
  146. playlistType?: PlaylistLevelType
  147. ) {
  148. if (this.timeRanges) {
  149. this.timeRanges[elementaryStream] = timeRange;
  150. }
  151. // Check if any flagged fragments have been unloaded
  152. Object.keys(this.fragments).forEach((key) => {
  153. const fragmentEntity = this.fragments[key];
  154. if (!fragmentEntity) {
  155. return;
  156. }
  157. if (!fragmentEntity.buffered && !fragmentEntity.loaded) {
  158. if (fragmentEntity.body.type === playlistType) {
  159. this.removeFragment(fragmentEntity.body);
  160. }
  161. return;
  162. }
  163. const esData = fragmentEntity.range[elementaryStream];
  164. if (!esData) {
  165. return;
  166. }
  167. esData.time.some((time: FragmentTimeRange) => {
  168. const isNotBuffered = !this.isTimeBuffered(
  169. time.startPTS,
  170. time.endPTS,
  171. timeRange
  172. );
  173. if (isNotBuffered) {
  174. // Unregister partial fragment as it needs to load again to be reused
  175. this.removeFragment(fragmentEntity.body);
  176. }
  177. return isNotBuffered;
  178. });
  179. });
  180. }
  181.  
  182. /**
  183. * Checks if the fragment passed in is loaded in the buffer properly
  184. * Partially loaded fragments will be registered as a partial fragment
  185. */
  186. private detectPartialFragments(data: FragBufferedData) {
  187. const timeRanges = this.timeRanges;
  188. const { frag, part } = data;
  189. if (!timeRanges || frag.sn === 'initSegment') {
  190. return;
  191. }
  192.  
  193. const fragKey = getFragmentKey(frag);
  194. const fragmentEntity = this.fragments[fragKey];
  195. if (!fragmentEntity) {
  196. return;
  197. }
  198. Object.keys(timeRanges).forEach((elementaryStream) => {
  199. const streamInfo = frag.elementaryStreams[elementaryStream];
  200. if (!streamInfo) {
  201. return;
  202. }
  203. const timeRange = timeRanges[elementaryStream];
  204. const partial = part !== null || streamInfo.partial === true;
  205. fragmentEntity.range[elementaryStream] = this.getBufferedTimes(
  206. frag,
  207. part,
  208. partial,
  209. timeRange
  210. );
  211. });
  212. fragmentEntity.loaded = null;
  213. if (Object.keys(fragmentEntity.range).length) {
  214. fragmentEntity.buffered = true;
  215. if (fragmentEntity.body.endList) {
  216. this.endListFragments[fragmentEntity.body.type] = fragmentEntity;
  217. }
  218. } else {
  219. // remove fragment if nothing was appended
  220. this.removeFragment(fragmentEntity.body);
  221. }
  222. }
  223.  
  224. public fragBuffered(frag: Fragment) {
  225. const fragKey = getFragmentKey(frag);
  226. const fragmentEntity = this.fragments[fragKey];
  227. if (fragmentEntity) {
  228. fragmentEntity.loaded = null;
  229. fragmentEntity.buffered = true;
  230. }
  231. }
  232.  
  233. private getBufferedTimes(
  234. fragment: Fragment,
  235. part: Part | null,
  236. partial: boolean,
  237. timeRange: TimeRanges
  238. ): FragmentBufferedRange {
  239. const buffered: FragmentBufferedRange = {
  240. time: [],
  241. partial,
  242. };
  243. const startPTS = part ? part.start : fragment.start;
  244. const endPTS = part ? part.end : fragment.end;
  245. const minEndPTS = fragment.minEndPTS || endPTS;
  246. const maxStartPTS = fragment.maxStartPTS || startPTS;
  247. for (let i = 0; i < timeRange.length; i++) {
  248. const startTime = timeRange.start(i) - this.bufferPadding;
  249. const endTime = timeRange.end(i) + this.bufferPadding;
  250. if (maxStartPTS >= startTime && minEndPTS <= endTime) {
  251. // Fragment is entirely contained in buffer
  252. // No need to check the other timeRange times since it's completely playable
  253. buffered.time.push({
  254. startPTS: Math.max(startPTS, timeRange.start(i)),
  255. endPTS: Math.min(endPTS, timeRange.end(i)),
  256. });
  257. break;
  258. } else if (startPTS < endTime && endPTS > startTime) {
  259. buffered.partial = true;
  260. // Check for intersection with buffer
  261. // Get playable sections of the fragment
  262. buffered.time.push({
  263. startPTS: Math.max(startPTS, timeRange.start(i)),
  264. endPTS: Math.min(endPTS, timeRange.end(i)),
  265. });
  266. } else if (endPTS <= startTime) {
  267. // No need to check the rest of the timeRange as it is in order
  268. break;
  269. }
  270. }
  271. return buffered;
  272. }
  273.  
  274. /**
  275. * Gets the partial fragment for a certain time
  276. */
  277. public getPartialFragment(time: number): Fragment | null {
  278. let bestFragment: Fragment | null = null;
  279. let timePadding: number;
  280. let startTime: number;
  281. let endTime: number;
  282. let bestOverlap: number = 0;
  283. const { bufferPadding, fragments } = this;
  284. Object.keys(fragments).forEach((key) => {
  285. const fragmentEntity = fragments[key];
  286. if (!fragmentEntity) {
  287. return;
  288. }
  289. if (isPartial(fragmentEntity)) {
  290. startTime = fragmentEntity.body.start - bufferPadding;
  291. endTime = fragmentEntity.body.end + bufferPadding;
  292. if (time >= startTime && time <= endTime) {
  293. // Use the fragment that has the most padding from start and end time
  294. timePadding = Math.min(time - startTime, endTime - time);
  295. if (bestOverlap <= timePadding) {
  296. bestFragment = fragmentEntity.body;
  297. bestOverlap = timePadding;
  298. }
  299. }
  300. }
  301. });
  302. return bestFragment;
  303. }
  304.  
  305. public isEndListAppended(type: PlaylistLevelType): boolean {
  306. const lastFragmentEntity = this.endListFragments[type];
  307. return (
  308. lastFragmentEntity !== undefined &&
  309. (lastFragmentEntity.buffered || isPartial(lastFragmentEntity))
  310. );
  311. }
  312.  
  313. public getState(fragment: Fragment): FragmentState {
  314. const fragKey = getFragmentKey(fragment);
  315. const fragmentEntity = this.fragments[fragKey];
  316.  
  317. if (fragmentEntity) {
  318. if (!fragmentEntity.buffered) {
  319. return FragmentState.APPENDING;
  320. } else if (isPartial(fragmentEntity)) {
  321. return FragmentState.PARTIAL;
  322. } else {
  323. return FragmentState.OK;
  324. }
  325. }
  326.  
  327. return FragmentState.NOT_LOADED;
  328. }
  329.  
  330. private isTimeBuffered(
  331. startPTS: number,
  332. endPTS: number,
  333. timeRange: TimeRanges
  334. ): boolean {
  335. let startTime;
  336. let endTime;
  337. for (let i = 0; i < timeRange.length; i++) {
  338. startTime = timeRange.start(i) - this.bufferPadding;
  339. endTime = timeRange.end(i) + this.bufferPadding;
  340. if (startPTS >= startTime && endPTS <= endTime) {
  341. return true;
  342. }
  343.  
  344. if (endPTS <= startTime) {
  345. // No need to check the rest of the timeRange as it is in order
  346. return false;
  347. }
  348. }
  349.  
  350. return false;
  351. }
  352.  
  353. private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
  354. const { frag, part } = data;
  355. // don't track initsegment (for which sn is not a number)
  356. // don't track frags used for bitrateTest, they're irrelevant.
  357. // don't track parts for memory efficiency
  358. if (frag.sn === 'initSegment' || frag.bitrateTest || part) {
  359. return;
  360. }
  361.  
  362. const fragKey = getFragmentKey(frag);
  363. this.fragments[fragKey] = {
  364. body: frag,
  365. loaded: data,
  366. buffered: false,
  367. range: Object.create(null),
  368. };
  369. }
  370.  
  371. private onBufferAppended(
  372. event: Events.BUFFER_APPENDED,
  373. data: BufferAppendedData
  374. ) {
  375. const { frag, part, timeRanges } = data;
  376. if (frag.type === PlaylistLevelType.MAIN) {
  377. if (this.activeFragment !== frag) {
  378. this.activeFragment = frag;
  379. frag.appendedPTS = undefined;
  380. }
  381. if (part) {
  382. let activeParts = this.activeParts;
  383. if (!activeParts) {
  384. this.activeParts = activeParts = [];
  385. }
  386. activeParts.push(part);
  387. } else {
  388. this.activeParts = null;
  389. }
  390. }
  391. // Store the latest timeRanges loaded in the buffer
  392. this.timeRanges = timeRanges;
  393. Object.keys(timeRanges).forEach((elementaryStream: SourceBufferName) => {
  394. const timeRange = timeRanges[elementaryStream] as TimeRanges;
  395. this.detectEvictedFragments(elementaryStream, timeRange);
  396. if (!part && frag.type === PlaylistLevelType.MAIN) {
  397. const streamInfo = frag.elementaryStreams[elementaryStream];
  398. if (!streamInfo) {
  399. return;
  400. }
  401. for (let i = 0; i < timeRange.length; i++) {
  402. const rangeEnd = timeRange.end(i);
  403. if (rangeEnd <= streamInfo.endPTS && rangeEnd > streamInfo.startPTS) {
  404. frag.appendedPTS = Math.max(rangeEnd, frag.appendedPTS || 0);
  405. } else {
  406. frag.appendedPTS = streamInfo.endPTS;
  407. }
  408. }
  409. }
  410. });
  411. }
  412.  
  413. private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
  414. this.detectPartialFragments(data);
  415. }
  416.  
  417. private hasFragment(fragment: Fragment): boolean {
  418. const fragKey = getFragmentKey(fragment);
  419. return !!this.fragments[fragKey];
  420. }
  421.  
  422. public removeFragmentsInRange(
  423. start: number,
  424. end: number,
  425. playlistType: PlaylistLevelType
  426. ) {
  427. Object.keys(this.fragments).forEach((key) => {
  428. const fragmentEntity = this.fragments[key];
  429. if (!fragmentEntity) {
  430. return;
  431. }
  432. if (fragmentEntity.buffered) {
  433. const frag = fragmentEntity.body;
  434. if (
  435. frag.type === playlistType &&
  436. frag.start < end &&
  437. frag.end > start
  438. ) {
  439. this.removeFragment(frag);
  440. }
  441. }
  442. });
  443. }
  444.  
  445. public removeFragment(fragment: Fragment) {
  446. const fragKey = getFragmentKey(fragment);
  447. fragment.stats.loaded = 0;
  448. fragment.clearElementaryStreamInfo();
  449. fragment.appendedPTS = undefined;
  450. delete this.fragments[fragKey];
  451. if (fragment.endList) {
  452. delete this.endListFragments[fragment.type];
  453. }
  454. }
  455.  
  456. public removeAllFragments() {
  457. this.fragments = Object.create(null);
  458. this.endListFragments = Object.create(null);
  459. this.activeFragment = null;
  460. this.activeParts = null;
  461. }
  462. }
  463.  
  464. function isPartial(fragmentEntity: FragmentEntity): boolean {
  465. return (
  466. fragmentEntity.buffered &&
  467. (fragmentEntity.range.video?.partial || fragmentEntity.range.audio?.partial)
  468. );
  469. }
  470.  
  471. function getFragmentKey(fragment: Fragment): string {
  472. return `${fragment.type}_${fragment.level}_${fragment.urlId}_${fragment.sn}`;
  473. }