download
raw
16.9 kB
import { Events } from '../events';
import type Hls from '../hls';
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
import type { SourceBufferName } from '../types/buffer';
import type { ComponentAPI } from '../types/component-api';
import type {
BufferAppendedData,
FragBufferedData,
FragLoadedData,
} from '../types/events';
import type {
FragmentBufferedRange,
FragmentEntity,
FragmentTimeRange,
} from '../types/fragment-tracker';
import type { PlaylistLevelType } from '../types/loader';
export const enum FragmentState {
NOT_LOADED = 'NOT_LOADED',
APPENDING = 'APPENDING',
PARTIAL = 'PARTIAL',
OK = 'OK',
}
export class FragmentTracker implements ComponentAPI {
private activePartLists: { [key in PlaylistLevelType]?: Part[] } =
Object.create(null);
private endListFragments: { [key in PlaylistLevelType]?: FragmentEntity } =
Object.create(null);
private fragments: Partial<Record<string, FragmentEntity>> =
Object.create(null);
private timeRanges:
| {
[key in SourceBufferName]?: TimeRanges;
}
| null = Object.create(null);
private bufferPadding: number = 0.2;
private hls: Hls | null;
private hasGaps: boolean = false;
constructor(hls: Hls) {
this.hls = hls;
this._registerListeners();
}
private _registerListeners() {
const { hls } = this;
if (hls) {
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
}
}
private _unregisterListeners() {
const { hls } = this;
if (hls) {
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
}
}
public destroy() {
this._unregisterListeners();
// @ts-ignore
this.hls =
// @ts-ignore
this.fragments =
// @ts-ignore
this.activePartLists =
// @ts-ignore
this.endListFragments =
this.timeRanges =
null;
}
/**
* Return a Fragment or Part with an appended range that matches the position and levelType
* Otherwise, return null
*/
public getAppendedFrag(
position: number,
levelType: PlaylistLevelType,
): MediaFragment | Part | null {
const activeParts = this.activePartLists[levelType];
if (activeParts) {
for (let i = activeParts.length; i--; ) {
const activePart = activeParts[i];
if (!activePart as any) {
break;
}
if (
activePart.start <= position &&
position <= activePart.end &&
activePart.loaded
) {
return activePart;
}
}
}
return this.getBufferedFrag(position, levelType);
}
/**
* Return a buffered Fragment that matches the position and levelType.
* A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted).
* If not found any Fragment, return null
*/
public getBufferedFrag(
position: number,
levelType: PlaylistLevelType,
): MediaFragment | null {
return this.getFragAtPos(position, levelType, true);
}
public getFragAtPos(
position: number,
levelType: PlaylistLevelType,
buffered?: boolean,
): MediaFragment | null {
const { fragments } = this;
const keys = Object.keys(fragments);
for (let i = keys.length; i--; ) {
const fragmentEntity = fragments[keys[i]];
if (
fragmentEntity?.body.type === levelType &&
(!buffered || fragmentEntity.buffered)
) {
const frag = fragmentEntity.body;
if (frag.start <= position && position <= frag.end) {
return frag;
}
}
}
return null;
}
/**
* 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)
*/
public detectEvictedFragments(
elementaryStream: SourceBufferName,
timeRange: TimeRanges,
playlistType: PlaylistLevelType,
appendedPart?: Part | null,
removeAppending?: boolean,
) {
if (this.timeRanges) {
this.timeRanges[elementaryStream] = timeRange;
}
// Check if any flagged fragments have been unloaded
// excluding anything newer than appendedPartSn
const appendedPartSn = appendedPart?.fragment.sn || -1;
Object.keys(this.fragments).forEach((key) => {
const fragmentEntity = this.fragments[key];
if (!fragmentEntity) {
return;
}
if (appendedPartSn >= fragmentEntity.body.sn) {
return;
}
if (
!fragmentEntity.buffered &&
(!fragmentEntity.loaded || removeAppending)
) {
if (fragmentEntity.body.type === playlistType) {
this.removeFragment(fragmentEntity.body);
}
return;
}
const esData = fragmentEntity.range[elementaryStream];
if (!esData) {
return;
}
if (esData.time.length === 0) {
this.removeFragment(fragmentEntity.body);
return;
}
esData.time.some((time: FragmentTimeRange) => {
const isNotBuffered = !this.isTimeBuffered(
time.startPTS,
time.endPTS,
timeRange,
);
if (isNotBuffered) {
// Unregister partial fragment as it needs to load again to be reused
this.removeFragment(fragmentEntity.body);
}
return isNotBuffered;
});
});
}
/**
* Checks if the fragment passed in is loaded in the buffer properly
* Partially loaded fragments will be registered as a partial fragment
*/
public detectPartialFragments(data: FragBufferedData) {
const timeRanges = this.timeRanges;
if (!timeRanges || data.frag.sn === 'initSegment') {
return;
}
const frag = data.frag as MediaFragment;
const fragKey = getFragmentKey(frag);
const fragmentEntity = this.fragments[fragKey];
if (!fragmentEntity || (fragmentEntity.buffered && frag.gap)) {
return;
}
const isFragHint = !frag.relurl;
Object.keys(timeRanges).forEach((elementaryStream: SourceBufferName) => {
const streamInfo = frag.elementaryStreams[elementaryStream];
if (!streamInfo) {
return;
}
const timeRange = timeRanges[elementaryStream] as TimeRanges;
const partial = isFragHint || streamInfo.partial === true;
fragmentEntity.range[elementaryStream] = this.getBufferedTimes(
frag,
data.part,
partial,
timeRange,
);
});
fragmentEntity.loaded = null;
if (Object.keys(fragmentEntity.range).length) {
this.bufferedEnd(fragmentEntity, frag);
if (!isPartial(fragmentEntity)) {
// Remove older fragment parts from lookup after frag is tracked as buffered
this.removeParts(frag.sn - 1, frag.type);
}
} else {
// remove fragment if nothing was appended
this.removeFragment(fragmentEntity.body);
}
}
private bufferedEnd(fragmentEntity: FragmentEntity, frag: MediaFragment) {
fragmentEntity.buffered = true;
const endList = (fragmentEntity.body.endList =
frag.endList || fragmentEntity.body.endList);
if (endList) {
this.endListFragments[fragmentEntity.body.type] = fragmentEntity;
}
}
private removeParts(snToKeep: number, levelType: PlaylistLevelType) {
const activeParts = this.activePartLists[levelType];
if (!activeParts) {
return;
}
this.activePartLists[levelType] = filterParts(
activeParts,
(part) => part.fragment.sn >= snToKeep,
);
}
public fragBuffered(frag: MediaFragment, force?: true) {
const fragKey = getFragmentKey(frag);
let fragmentEntity = this.fragments[fragKey];
if (!fragmentEntity && force) {
fragmentEntity = this.fragments[fragKey] = {
body: frag,
appendedPTS: null,
loaded: null,
buffered: false,
range: Object.create(null),
};
if (frag.gap) {
this.hasGaps = true;
}
}
if (fragmentEntity) {
fragmentEntity.loaded = null;
this.bufferedEnd(fragmentEntity, frag);
}
}
private getBufferedTimes(
fragment: Fragment,
part: Part | null,
partial: boolean,
timeRange: TimeRanges,
): FragmentBufferedRange {
const buffered: FragmentBufferedRange = {
time: [],
partial,
};
const startPTS = fragment.start;
const endPTS = fragment.end;
const minEndPTS = fragment.minEndPTS || endPTS;
const maxStartPTS = fragment.maxStartPTS || startPTS;
for (let i = 0; i < timeRange.length; i++) {
const startTime = timeRange.start(i) - this.bufferPadding;
const endTime = timeRange.end(i) + this.bufferPadding;
if (maxStartPTS >= startTime && minEndPTS <= endTime) {
// Fragment is entirely contained in buffer
// No need to check the other timeRange times since it's completely playable
buffered.time.push({
startPTS: Math.max(startPTS, timeRange.start(i)),
endPTS: Math.min(endPTS, timeRange.end(i)),
});
break;
} else if (startPTS < endTime && endPTS > startTime) {
const start = Math.max(startPTS, timeRange.start(i));
const end = Math.min(endPTS, timeRange.end(i));
if (end > start) {
buffered.partial = true;
// Check for intersection with buffer
// Get playable sections of the fragment
buffered.time.push({
startPTS: start,
endPTS: end,
});
}
} else if (endPTS <= startTime) {
// No need to check the rest of the timeRange as it is in order
break;
}
}
return buffered;
}
/**
* Gets the partial fragment for a certain time
*/
public getPartialFragment(time: number): MediaFragment | null {
let bestFragment: Fragment | null = null;
let timePadding: number;
let startTime: number;
let endTime: number;
let bestOverlap: number = 0;
const { bufferPadding, fragments } = this;
Object.keys(fragments).forEach((key) => {
const fragmentEntity = fragments[key];
if (!fragmentEntity) {
return;
}
if (isPartial(fragmentEntity)) {
startTime = fragmentEntity.body.start - bufferPadding;
endTime = fragmentEntity.body.end + 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;
}
public isEndListAppended(type: PlaylistLevelType): boolean {
const lastFragmentEntity = this.endListFragments[type];
return (
lastFragmentEntity !== undefined &&
(lastFragmentEntity.buffered || isPartial(lastFragmentEntity))
);
}
public getState(fragment: Fragment): FragmentState {
const fragKey = getFragmentKey(fragment);
const fragmentEntity = this.fragments[fragKey];
if (fragmentEntity) {
if (!fragmentEntity.buffered) {
return FragmentState.APPENDING;
} else if (isPartial(fragmentEntity)) {
return FragmentState.PARTIAL;
} else {
return FragmentState.OK;
}
}
return FragmentState.NOT_LOADED;
}
private isTimeBuffered(
startPTS: number,
endPTS: number,
timeRange: TimeRanges,
): boolean {
let startTime;
let 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;
}
private onManifestLoading() {
this.removeAllFragments();
}
private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
// don't track initsegment (for which sn is not a number)
// don't track frags used for bitrateTest, they're irrelevant.
if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) {
return;
}
const frag = data.frag as MediaFragment;
// Fragment entity `loaded` FragLoadedData is null when loading parts
const loaded = data.part ? null : data;
const fragKey = getFragmentKey(frag);
this.fragments[fragKey] = {
body: frag,
appendedPTS: null,
loaded,
buffered: false,
range: Object.create(null),
};
}
private onBufferAppended(
event: Events.BUFFER_APPENDED,
data: BufferAppendedData,
) {
const { frag, part, timeRanges, type } = data;
if (frag.sn === 'initSegment') {
return;
}
const playlistType = frag.type;
if (part) {
let activeParts = this.activePartLists[playlistType];
if (!activeParts) {
this.activePartLists[playlistType] = activeParts = [];
}
activeParts.push(part);
}
// Store the latest timeRanges loaded in the buffer
this.timeRanges = timeRanges;
const timeRange = timeRanges[type] as TimeRanges;
this.detectEvictedFragments(type, timeRange, playlistType, part);
}
private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
this.detectPartialFragments(data);
}
private hasFragment(fragment: Fragment): boolean {
const fragKey = getFragmentKey(fragment);
return !!this.fragments[fragKey];
}
public hasFragments(type?: PlaylistLevelType): boolean {
const { fragments } = this;
const keys = Object.keys(fragments);
if (!type) {
return keys.length > 0;
}
for (let i = keys.length; i--; ) {
const fragmentEntity = fragments[keys[i]];
if (fragmentEntity?.body.type === type) {
return true;
}
}
return false;
}
public hasParts(type: PlaylistLevelType): boolean {
return !!this.activePartLists[type]?.length;
}
public removeFragmentsInRange(
start: number,
end: number,
playlistType: PlaylistLevelType,
withGapOnly?: boolean,
unbufferedOnly?: boolean,
) {
if (withGapOnly && !this.hasGaps) {
return;
}
Object.keys(this.fragments).forEach((key) => {
const fragmentEntity = this.fragments[key];
if (!fragmentEntity) {
return;
}
const frag = fragmentEntity.body;
if (frag.type !== playlistType || (withGapOnly && !frag.gap)) {
return;
}
if (
frag.start < end &&
frag.end > start &&
(fragmentEntity.buffered || unbufferedOnly)
) {
this.removeFragment(frag);
}
});
}
public removeFragment(fragment: Fragment) {
const fragKey = getFragmentKey(fragment);
fragment.clearElementaryStreamInfo();
const activeParts = this.activePartLists[fragment.type];
if (activeParts) {
const snToRemove = fragment.sn;
this.activePartLists[fragment.type] = filterParts(
activeParts,
(part) => part.fragment.sn !== snToRemove,
);
}
delete this.fragments[fragKey];
if (fragment.endList) {
delete this.endListFragments[fragment.type];
}
}
public removeAllFragments() {
this.fragments = Object.create(null);
this.endListFragments = Object.create(null);
this.activePartLists = Object.create(null);
this.hasGaps = false;
const partlist = this.hls?.latestLevelDetails?.partList;
if (partlist) {
partlist.forEach((part) => part.clearElementaryStreamInfo());
}
}
}
function isPartial(fragmentEntity: FragmentEntity): boolean {
return (
fragmentEntity.buffered &&
!!(
fragmentEntity.body.gap ||
fragmentEntity.range.video?.partial ||
fragmentEntity.range.audio?.partial ||
fragmentEntity.range.audiovideo?.partial
)
);
}
function getFragmentKey(fragment: Fragment): string {
return `${fragment.type}_${fragment.level}_${fragment.sn}`;
}
function filterParts(partList: Part[], predicate: (part: Part) => boolean) {
return partList.filter((part) => {
const keep = predicate(part);
if (!keep) {
part.clearElementaryStreamInfo();
}
return keep;
});
}

Xet Storage Details

Size:
16.9 kB
·
Xet hash:
4aa010948d8a8eb9ee433d5161c6856a46b04ce52d0b787b058cb7aed402b3de

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.