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