src/controller/fragment-tracker.js
import EventHandler from '../event-handler';
import Event from '../events';
export const FragmentState = {
NOT_LOADED: 'NOT_LOADED',
APPENDING: 'APPENDING',
PARTIAL: 'PARTIAL',
OK: 'OK'
};
export class FragmentTracker extends EventHandler {
constructor (hls) {
super(hls,
Event.BUFFER_APPENDED,
Event.FRAG_BUFFERED,
Event.FRAG_LOADED
);
this.bufferPadding = 0.2;
this.fragments = Object.create(null);
this.timeRanges = Object.create(null);
this.config = hls.config;
}
destroy () {
this.fragments = Object.create(null);
this.timeRanges = Object.create(null);
this.config = null;
EventHandler.prototype.destroy.call(this);
super.destroy();
}
/**
* Return a Fragment that match the position and levelType.
* If not found any Fragment, return null
* @param {number} position
* @param {LevelType} levelType
* @returns {Fragment|null}
*/
getBufferedFrag (position, levelType) {
const fragments = this.fragments;
const bufferedFrags = Object.keys(fragments).filter(key => {
const fragmentEntity = fragments[key];
if (fragmentEntity.body.type !== levelType) {
return false;
}
if (!fragmentEntity.buffered) {
return false;
}
const frag = fragmentEntity.body;
return frag.startPTS <= position && position <= frag.endPTS;
});
if (bufferedFrags.length === 0) {
return null;
} else {
// https://github.com/video-dev/hls.js/pull/1545#discussion_r166229566
const bufferedFragKey = bufferedFrags.pop();
return fragments[bufferedFragKey].body;
}
}
/**
* Partial fragments effected by coded frame eviction will be removed
* The browser will unload parts of the buffer to free up memory for new buffer data
* 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)
* @param {String} elementaryStream The elementaryStream of media this is (eg. video/audio)
* @param {TimeRanges} timeRange TimeRange object from a sourceBuffer
*/
detectEvictedFragments (elementaryStream, timeRange) {
// Check if any flagged fragments have been unloaded
Object.keys(this.fragments).forEach(key => {
const fragmentEntity = this.fragments[key];
if (!fragmentEntity || !fragmentEntity.buffered) {
return;
}
const esData = fragmentEntity.range[elementaryStream];
if (!esData) {
return;
}
const fragmentTimes = esData.time;
for (let i = 0; i < fragmentTimes.length; i++) {
const time = fragmentTimes[i];
if (!this.isTimeBuffered(time.startPTS, time.endPTS, timeRange)) {
// Unregister partial fragment as it needs to load again to be reused
this.removeFragment(fragmentEntity.body);
break;
}
}
});
}
/**
* Checks if the fragment passed in is loaded in the buffer properly
* Partially loaded fragments will be registered as a partial fragment
* @param {Object} fragment Check the fragment against all sourceBuffers loaded
*/
detectPartialFragments (fragment) {
let fragKey = this.getFragmentKey(fragment);
let fragmentEntity = this.fragments[fragKey];
if (fragmentEntity) {
fragmentEntity.buffered = true;
Object.keys(this.timeRanges).forEach(elementaryStream => {
if (fragment.hasElementaryStream(elementaryStream)) {
let timeRange = this.timeRanges[elementaryStream];
// Check for malformed fragments
// Gaps need to be calculated for each elementaryStream
fragmentEntity.range[elementaryStream] = this.getBufferedTimes(fragment.startPTS, fragment.endPTS, timeRange);
}
});
}
}
getBufferedTimes (startPTS, endPTS, timeRange) {
let fragmentTimes = [];
let startTime, endTime;
let fragmentPartial = false;
for (let i = 0; i < timeRange.length; i++) {
startTime = timeRange.start(i) - this.bufferPadding;
endTime = timeRange.end(i) + this.bufferPadding;
if (startPTS >= startTime && endPTS <= endTime) {
// Fragment is entirely contained in buffer
// No need to check the other timeRange times since it's completely playable
fragmentTimes.push({
startPTS: Math.max(startPTS, timeRange.start(i)),
endPTS: Math.min(endPTS, timeRange.end(i))
});
break;
} else if (startPTS < endTime && endPTS > startTime) {
// Check for intersection with buffer
// Get playable sections of the fragment
fragmentTimes.push({
startPTS: Math.max(startPTS, timeRange.start(i)),
endPTS: Math.min(endPTS, timeRange.end(i))
});
fragmentPartial = true;
} else if (endPTS <= startTime) {
// No need to check the rest of the timeRange as it is in order
break;
}
}
return {
time: fragmentTimes,
partial: fragmentPartial
};
}
getFragmentKey (fragment) {
return `${fragment.type}_${fragment.level}_${fragment.urlId}_${fragment.sn}`;
}
/**
* Gets the partial fragment for a certain time
* @param {Number} time
* @returns {Object} fragment Returns a partial fragment at a time or null if there is no partial fragment
*/
getPartialFragment (time) {
let timePadding, startTime, endTime;
let bestFragment = null;
let bestOverlap = 0;
Object.keys(this.fragments).forEach(key => {
const fragmentEntity = this.fragments[key];
if (this.isPartial(fragmentEntity)) {
startTime = fragmentEntity.body.startPTS - this.bufferPadding;
endTime = fragmentEntity.body.endPTS + this.bufferPadding;
if (time >= startTime && time <= endTime) {
// Use the fragment that has the most padding from start and end time
timePadding = Math.min(time - startTime, endTime - time);
if (bestOverlap <= timePadding) {
bestFragment = fragmentEntity.body;
bestOverlap = timePadding;
}
}
}
});
return bestFragment;
}
/**
* @param {Object} fragment The fragment to check
* @returns {String} Returns the fragment state when a fragment never loaded or if it partially loaded
*/
getState (fragment) {
let fragKey = this.getFragmentKey(fragment);
let fragmentEntity = this.fragments[fragKey];
let state = FragmentState.NOT_LOADED;
if (fragmentEntity !== undefined) {
if (!fragmentEntity.buffered) {
state = FragmentState.APPENDING;
} else if (this.isPartial(fragmentEntity) === true) {
state = FragmentState.PARTIAL;
} else {
state = FragmentState.OK;
}
}
return state;
}
isPartial (fragmentEntity) {
return fragmentEntity.buffered === true &&
((fragmentEntity.range.video !== undefined && fragmentEntity.range.video.partial === true) ||
(fragmentEntity.range.audio !== undefined && fragmentEntity.range.audio.partial === true));
}
isTimeBuffered (startPTS, endPTS, timeRange) {
let startTime, endTime;
for (let i = 0; i < timeRange.length; i++) {
startTime = timeRange.start(i) - this.bufferPadding;
endTime = timeRange.end(i) + this.bufferPadding;
if (startPTS >= startTime && endPTS <= endTime) {
return true;
}
if (endPTS <= startTime) {
// No need to check the rest of the timeRange as it is in order
return false;
}
}
return false;
}
/**
* Fires when a fragment loading is completed
*/
onFragLoaded (e) {
const fragment = e.frag;
// don't track initsegment (for which sn is not a number)
// don't track frags used for bitrateTest, they're irrelevant.
if (!Number.isFinite(fragment.sn) || fragment.bitrateTest) {
return;
}
this.fragments[this.getFragmentKey(fragment)] = {
body: fragment,
range: Object.create(null),
buffered: false
};
}
/**
* Fires when the buffer is updated
*/
onBufferAppended (e) {
// Store the latest timeRanges loaded in the buffer
this.timeRanges = e.timeRanges;
Object.keys(this.timeRanges).forEach(elementaryStream => {
let timeRange = this.timeRanges[elementaryStream];
this.detectEvictedFragments(elementaryStream, timeRange);
});
}
/**
* Fires after a fragment has been loaded into the source buffer
*/
onFragBuffered (e) {
this.detectPartialFragments(e.frag);
}
/**
* Return true if fragment tracker has the fragment.
* @param {Object} fragment
* @returns {boolean}
*/
hasFragment (fragment) {
const fragKey = this.getFragmentKey(fragment);
return this.fragments[fragKey] !== undefined;
}
/**
* Remove a fragment from fragment tracker until it is loaded again
* @param {Object} fragment The fragment to remove
*/
removeFragment (fragment) {
let fragKey = this.getFragmentKey(fragment);
delete this.fragments[fragKey];
}
/**
* Remove all fragments from fragment tracker.
*/
removeAllFragments () {
this.fragments = Object.create(null);
}
}