Home Reference Source

src/controller/fragment-tracker.js

  1. import EventHandler from '../event-handler';
  2. import Event from '../events';
  3.  
  4. export const FragmentState = {
  5. NOT_LOADED: 'NOT_LOADED',
  6. APPENDING: 'APPENDING',
  7. PARTIAL: 'PARTIAL',
  8. OK: 'OK'
  9. };
  10.  
  11. export class FragmentTracker extends EventHandler {
  12. constructor (hls) {
  13. super(hls,
  14. Event.BUFFER_APPENDED,
  15. Event.FRAG_BUFFERED,
  16. Event.FRAG_LOADED
  17. );
  18.  
  19. this.bufferPadding = 0.2;
  20.  
  21. this.fragments = Object.create(null);
  22. this.timeRanges = Object.create(null);
  23.  
  24. this.config = hls.config;
  25. }
  26.  
  27. destroy () {
  28. this.fragments = Object.create(null);
  29. this.timeRanges = Object.create(null);
  30. this.config = null;
  31. EventHandler.prototype.destroy.call(this);
  32. super.destroy();
  33. }
  34.  
  35. /**
  36. * Return a Fragment that match the position and levelType.
  37. * If not found any Fragment, return null
  38. * @param {number} position
  39. * @param {LevelType} levelType
  40. * @returns {Fragment|null}
  41. */
  42. getBufferedFrag (position, levelType) {
  43. const fragments = this.fragments;
  44. const bufferedFrags = Object.keys(fragments).filter(key => {
  45. const fragmentEntity = fragments[key];
  46. if (fragmentEntity.body.type !== levelType) {
  47. return false;
  48. }
  49.  
  50. if (!fragmentEntity.buffered) {
  51. return false;
  52. }
  53.  
  54. const frag = fragmentEntity.body;
  55. return frag.startPTS <= position && position <= frag.endPTS;
  56. });
  57. if (bufferedFrags.length === 0) {
  58. return null;
  59. } else {
  60. // https://github.com/video-dev/hls.js/pull/1545#discussion_r166229566
  61. const bufferedFragKey = bufferedFrags.pop();
  62. return fragments[bufferedFragKey].body;
  63. }
  64. }
  65.  
  66. /**
  67. * Partial fragments effected by coded frame eviction will be removed
  68. * The browser will unload parts of the buffer to free up memory for new buffer data
  69. * 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)
  70. * @param {String} elementaryStream The elementaryStream of media this is (eg. video/audio)
  71. * @param {TimeRanges} timeRange TimeRange object from a sourceBuffer
  72. */
  73. detectEvictedFragments (elementaryStream, timeRange) {
  74. // Check if any flagged fragments have been unloaded
  75. Object.keys(this.fragments).forEach(key => {
  76. const fragmentEntity = this.fragments[key];
  77. if (!fragmentEntity || !fragmentEntity.buffered) {
  78. return;
  79. }
  80. const esData = fragmentEntity.range[elementaryStream];
  81. if (!esData) {
  82. return;
  83. }
  84. const fragmentTimes = esData.time;
  85. for (let i = 0; i < fragmentTimes.length; i++) {
  86. const time = fragmentTimes[i];
  87. if (!this.isTimeBuffered(time.startPTS, time.endPTS, timeRange)) {
  88. // Unregister partial fragment as it needs to load again to be reused
  89. this.removeFragment(fragmentEntity.body);
  90. break;
  91. }
  92. }
  93. });
  94. }
  95.  
  96. /**
  97. * Checks if the fragment passed in is loaded in the buffer properly
  98. * Partially loaded fragments will be registered as a partial fragment
  99. * @param {Object} fragment Check the fragment against all sourceBuffers loaded
  100. */
  101. detectPartialFragments (fragment) {
  102. let fragKey = this.getFragmentKey(fragment);
  103. let fragmentEntity = this.fragments[fragKey];
  104. if (fragmentEntity) {
  105. fragmentEntity.buffered = true;
  106.  
  107. Object.keys(this.timeRanges).forEach(elementaryStream => {
  108. if (fragment.hasElementaryStream(elementaryStream)) {
  109. let timeRange = this.timeRanges[elementaryStream];
  110. // Check for malformed fragments
  111. // Gaps need to be calculated for each elementaryStream
  112. fragmentEntity.range[elementaryStream] = this.getBufferedTimes(fragment.startPTS, fragment.endPTS, timeRange);
  113. }
  114. });
  115. }
  116. }
  117.  
  118. getBufferedTimes (startPTS, endPTS, timeRange) {
  119. let fragmentTimes = [];
  120. let startTime, endTime;
  121. let fragmentPartial = false;
  122. for (let i = 0; i < timeRange.length; i++) {
  123. startTime = timeRange.start(i) - this.bufferPadding;
  124. endTime = timeRange.end(i) + this.bufferPadding;
  125. if (startPTS >= startTime && endPTS <= endTime) {
  126. // Fragment is entirely contained in buffer
  127. // No need to check the other timeRange times since it's completely playable
  128. fragmentTimes.push({
  129. startPTS: Math.max(startPTS, timeRange.start(i)),
  130. endPTS: Math.min(endPTS, timeRange.end(i))
  131. });
  132. break;
  133. } else if (startPTS < endTime && endPTS > startTime) {
  134. // Check for intersection with buffer
  135. // Get playable sections of the fragment
  136. fragmentTimes.push({
  137. startPTS: Math.max(startPTS, timeRange.start(i)),
  138. endPTS: Math.min(endPTS, timeRange.end(i))
  139. });
  140. fragmentPartial = true;
  141. } else if (endPTS <= startTime) {
  142. // No need to check the rest of the timeRange as it is in order
  143. break;
  144. }
  145. }
  146.  
  147. return {
  148. time: fragmentTimes,
  149. partial: fragmentPartial
  150. };
  151. }
  152.  
  153. getFragmentKey (fragment) {
  154. return `${fragment.type}_${fragment.level}_${fragment.urlId}_${fragment.sn}`;
  155. }
  156.  
  157. /**
  158. * Gets the partial fragment for a certain time
  159. * @param {Number} time
  160. * @returns {Object} fragment Returns a partial fragment at a time or null if there is no partial fragment
  161. */
  162. getPartialFragment (time) {
  163. let timePadding, startTime, endTime;
  164. let bestFragment = null;
  165. let bestOverlap = 0;
  166. Object.keys(this.fragments).forEach(key => {
  167. const fragmentEntity = this.fragments[key];
  168. if (this.isPartial(fragmentEntity)) {
  169. startTime = fragmentEntity.body.startPTS - this.bufferPadding;
  170. endTime = fragmentEntity.body.endPTS + this.bufferPadding;
  171. if (time >= startTime && time <= endTime) {
  172. // Use the fragment that has the most padding from start and end time
  173. timePadding = Math.min(time - startTime, endTime - time);
  174. if (bestOverlap <= timePadding) {
  175. bestFragment = fragmentEntity.body;
  176. bestOverlap = timePadding;
  177. }
  178. }
  179. }
  180. });
  181. return bestFragment;
  182. }
  183.  
  184. /**
  185. * @param {Object} fragment The fragment to check
  186. * @returns {String} Returns the fragment state when a fragment never loaded or if it partially loaded
  187. */
  188. getState (fragment) {
  189. let fragKey = this.getFragmentKey(fragment);
  190. let fragmentEntity = this.fragments[fragKey];
  191. let state = FragmentState.NOT_LOADED;
  192.  
  193. if (fragmentEntity !== undefined) {
  194. if (!fragmentEntity.buffered) {
  195. state = FragmentState.APPENDING;
  196. } else if (this.isPartial(fragmentEntity) === true) {
  197. state = FragmentState.PARTIAL;
  198. } else {
  199. state = FragmentState.OK;
  200. }
  201. }
  202.  
  203. return state;
  204. }
  205.  
  206. isPartial (fragmentEntity) {
  207. return fragmentEntity.buffered === true &&
  208. ((fragmentEntity.range.video !== undefined && fragmentEntity.range.video.partial === true) ||
  209. (fragmentEntity.range.audio !== undefined && fragmentEntity.range.audio.partial === true));
  210. }
  211.  
  212. isTimeBuffered (startPTS, endPTS, timeRange) {
  213. let startTime, endTime;
  214. for (let i = 0; i < timeRange.length; i++) {
  215. startTime = timeRange.start(i) - this.bufferPadding;
  216. endTime = timeRange.end(i) + this.bufferPadding;
  217. if (startPTS >= startTime && endPTS <= endTime) {
  218. return true;
  219. }
  220.  
  221. if (endPTS <= startTime) {
  222. // No need to check the rest of the timeRange as it is in order
  223. return false;
  224. }
  225. }
  226.  
  227. return false;
  228. }
  229.  
  230. /**
  231. * Fires when a fragment loading is completed
  232. */
  233. onFragLoaded (e) {
  234. const fragment = e.frag;
  235. // don't track initsegment (for which sn is not a number)
  236. // don't track frags used for bitrateTest, they're irrelevant.
  237. if (!Number.isFinite(fragment.sn) || fragment.bitrateTest) {
  238. return;
  239. }
  240.  
  241. this.fragments[this.getFragmentKey(fragment)] = {
  242. body: fragment,
  243. range: Object.create(null),
  244. buffered: false
  245. };
  246. }
  247.  
  248. /**
  249. * Fires when the buffer is updated
  250. */
  251. onBufferAppended (e) {
  252. // Store the latest timeRanges loaded in the buffer
  253. this.timeRanges = e.timeRanges;
  254. Object.keys(this.timeRanges).forEach(elementaryStream => {
  255. let timeRange = this.timeRanges[elementaryStream];
  256. this.detectEvictedFragments(elementaryStream, timeRange);
  257. });
  258. }
  259.  
  260. /**
  261. * Fires after a fragment has been loaded into the source buffer
  262. */
  263. onFragBuffered (e) {
  264. this.detectPartialFragments(e.frag);
  265. }
  266.  
  267. /**
  268. * Return true if fragment tracker has the fragment.
  269. * @param {Object} fragment
  270. * @returns {boolean}
  271. */
  272. hasFragment (fragment) {
  273. const fragKey = this.getFragmentKey(fragment);
  274. return this.fragments[fragKey] !== undefined;
  275. }
  276.  
  277. /**
  278. * Remove a fragment from fragment tracker until it is loaded again
  279. * @param {Object} fragment The fragment to remove
  280. */
  281. removeFragment (fragment) {
  282. let fragKey = this.getFragmentKey(fragment);
  283. delete this.fragments[fragKey];
  284. }
  285.  
  286. /**
  287. * Remove all fragments from fragment tracker.
  288. */
  289. removeAllFragments () {
  290. this.fragments = Object.create(null);
  291. }
  292. }