src/utils/buffer-helper.ts
/**
* @module BufferHelper
*
* Providing methods dealing with buffer length retrieval for example.
*
* In general, a helper around HTML5 MediaElement TimeRanges gathered from `buffered` property.
*
* Also @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered
*/
import { logger } from '../utils/logger';
type BufferTimeRange = {
start: number
end: number
};
type Bufferable = {
buffered: TimeRanges
};
const noopBuffered: TimeRanges = {
length: 0,
start: () => 0,
end: () => 0
};
export class BufferHelper {
/**
* Return true if `media`'s buffered include `position`
* @param {Bufferable} media
* @param {number} position
* @returns {boolean}
*/
static isBuffered (media: Bufferable, position: number): boolean {
try {
if (media) {
let buffered = BufferHelper.getBuffered(media);
for (let i = 0; i < buffered.length; i++) {
if (position >= buffered.start(i) && position <= buffered.end(i)) {
return true;
}
}
}
} catch (error) {
// this is to catch
// InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
// This SourceBuffer has been removed from the parent media source
}
return false;
}
static bufferInfo (
media: Bufferable,
pos: number,
maxHoleDuration: number
): {
len: number,
start: number,
end: number,
nextStart?: number,
} {
try {
if (media) {
let vbuffered = BufferHelper.getBuffered(media);
let buffered: BufferTimeRange[] = [];
let i: number;
for (i = 0; i < vbuffered.length; i++) {
buffered.push({ start: vbuffered.start(i), end: vbuffered.end(i) });
}
return this.bufferedInfo(buffered, pos, maxHoleDuration);
}
} catch (error) {
// this is to catch
// InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer':
// This SourceBuffer has been removed from the parent media source
}
return { len: 0, start: pos, end: pos, nextStart: undefined };
}
static bufferedInfo (
buffered: BufferTimeRange[],
pos: number,
maxHoleDuration: number
): {
len: number,
start: number,
end: number,
nextStart?: number,
} {
// sort on buffer.start/smaller end (IE does not always return sorted buffered range)
buffered.sort(function (a, b) {
let diff = a.start - b.start;
if (diff) {
return diff;
} else {
return b.end - a.end;
}
});
let buffered2: BufferTimeRange[] = [];
if (maxHoleDuration) {
// there might be some small holes between buffer time range
// consider that holes smaller than maxHoleDuration are irrelevant and build another
// buffer time range representations that discards those holes
for (let i = 0; i < buffered.length; i++) {
let buf2len = buffered2.length;
if (buf2len) {
let buf2end = buffered2[buf2len - 1].end;
// if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
if ((buffered[i].start - buf2end) < maxHoleDuration) {
// merge overlapping time ranges
// update lastRange.end only if smaller than item.end
// e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
// whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
if (buffered[i].end > buf2end) {
buffered2[buf2len - 1].end = buffered[i].end;
}
} else {
// big hole
buffered2.push(buffered[i]);
}
} else {
// first value
buffered2.push(buffered[i]);
}
}
} else {
buffered2 = buffered;
}
let bufferLen = 0;
// bufferStartNext can possibly be undefined based on the conditional logic below
let bufferStartNext: number | undefined;
// bufferStart and bufferEnd are buffer boundaries around current video position
let bufferStart: number = pos;
let bufferEnd: number = pos;
for (let i = 0; i < buffered2.length; i++) {
let start = buffered2[i].start,
end = buffered2[i].end;
// logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
if ((pos + maxHoleDuration) >= start && pos < end) {
// play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
bufferStart = start;
bufferEnd = end;
bufferLen = bufferEnd - pos;
} else if ((pos + maxHoleDuration) < start) {
bufferStartNext = start;
break;
}
}
return { len: bufferLen, start: bufferStart, end: bufferEnd, nextStart: bufferStartNext };
}
/**
* Safe method to get buffered property.
* SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource
*/
static getBuffered (media: Bufferable): TimeRanges {
try {
return media.buffered;
} catch (e) {
logger.log('failed to get media.buffered', e);
return noopBuffered;
}
}
}