379 lines
13 KiB
JavaScript
379 lines
13 KiB
JavaScript
/**
|
|
* ranges
|
|
*
|
|
* Utilities for working with TimeRanges.
|
|
*
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', {
|
|
value: true
|
|
});
|
|
|
|
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
|
|
|
var _videoJs = require('video.js');
|
|
|
|
var _videoJs2 = _interopRequireDefault(_videoJs);
|
|
|
|
// Fudge factor to account for TimeRanges rounding
|
|
var TIME_FUDGE_FACTOR = 1 / 30;
|
|
// Comparisons between time values such as current time and the end of the buffered range
|
|
// can be misleading because of precision differences or when the current media has poorly
|
|
// aligned audio and video, which can cause values to be slightly off from what you would
|
|
// expect. This value is what we consider to be safe to use in such comparisons to account
|
|
// for these scenarios.
|
|
var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
|
|
|
|
/**
|
|
* Clamps a value to within a range
|
|
* @param {Number} num - the value to clamp
|
|
* @param {Number} start - the start of the range to clamp within, inclusive
|
|
* @param {Number} end - the end of the range to clamp within, inclusive
|
|
* @return {Number}
|
|
*/
|
|
var clamp = function clamp(num, _ref) {
|
|
var _ref2 = _slicedToArray(_ref, 2);
|
|
|
|
var start = _ref2[0];
|
|
var end = _ref2[1];
|
|
|
|
return Math.min(Math.max(start, num), end);
|
|
};
|
|
var filterRanges = function filterRanges(timeRanges, predicate) {
|
|
var results = [];
|
|
var i = undefined;
|
|
|
|
if (timeRanges && timeRanges.length) {
|
|
// Search for ranges that match the predicate
|
|
for (i = 0; i < timeRanges.length; i++) {
|
|
if (predicate(timeRanges.start(i), timeRanges.end(i))) {
|
|
results.push([timeRanges.start(i), timeRanges.end(i)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return _videoJs2['default'].createTimeRanges(results);
|
|
};
|
|
|
|
/**
|
|
* Attempts to find the buffered TimeRange that contains the specified
|
|
* time.
|
|
* @param {TimeRanges} buffered - the TimeRanges object to query
|
|
* @param {number} time - the time to filter on.
|
|
* @returns {TimeRanges} a new TimeRanges object
|
|
*/
|
|
var findRange = function findRange(buffered, time) {
|
|
return filterRanges(buffered, function (start, end) {
|
|
return start - TIME_FUDGE_FACTOR <= time && end + TIME_FUDGE_FACTOR >= time;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns the TimeRanges that begin later than the specified time.
|
|
* @param {TimeRanges} timeRanges - the TimeRanges object to query
|
|
* @param {number} time - the time to filter on.
|
|
* @returns {TimeRanges} a new TimeRanges object.
|
|
*/
|
|
var findNextRange = function findNextRange(timeRanges, time) {
|
|
return filterRanges(timeRanges, function (start) {
|
|
return start - TIME_FUDGE_FACTOR >= time;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns gaps within a list of TimeRanges
|
|
* @param {TimeRanges} buffered - the TimeRanges object
|
|
* @return {TimeRanges} a TimeRanges object of gaps
|
|
*/
|
|
var findGaps = function findGaps(buffered) {
|
|
if (buffered.length < 2) {
|
|
return _videoJs2['default'].createTimeRanges();
|
|
}
|
|
|
|
var ranges = [];
|
|
|
|
for (var i = 1; i < buffered.length; i++) {
|
|
var start = buffered.end(i - 1);
|
|
var end = buffered.start(i);
|
|
|
|
ranges.push([start, end]);
|
|
}
|
|
|
|
return _videoJs2['default'].createTimeRanges(ranges);
|
|
};
|
|
|
|
/**
|
|
* Search for a likely end time for the segment that was just appened
|
|
* based on the state of the `buffered` property before and after the
|
|
* append. If we fin only one such uncommon end-point return it.
|
|
* @param {TimeRanges} original - the buffered time ranges before the update
|
|
* @param {TimeRanges} update - the buffered time ranges after the update
|
|
* @returns {Number|null} the end time added between `original` and `update`,
|
|
* or null if one cannot be unambiguously determined.
|
|
*/
|
|
var findSoleUncommonTimeRangesEnd = function findSoleUncommonTimeRangesEnd(original, update) {
|
|
var i = undefined;
|
|
var start = undefined;
|
|
var end = undefined;
|
|
var result = [];
|
|
var edges = [];
|
|
|
|
// In order to qualify as a possible candidate, the end point must:
|
|
// 1) Not have already existed in the `original` ranges
|
|
// 2) Not result from the shrinking of a range that already existed
|
|
// in the `original` ranges
|
|
// 3) Not be contained inside of a range that existed in `original`
|
|
var overlapsCurrentEnd = function overlapsCurrentEnd(span) {
|
|
return span[0] <= end && span[1] >= end;
|
|
};
|
|
|
|
if (original) {
|
|
// Save all the edges in the `original` TimeRanges object
|
|
for (i = 0; i < original.length; i++) {
|
|
start = original.start(i);
|
|
end = original.end(i);
|
|
|
|
edges.push([start, end]);
|
|
}
|
|
}
|
|
|
|
if (update) {
|
|
// Save any end-points in `update` that are not in the `original`
|
|
// TimeRanges object
|
|
for (i = 0; i < update.length; i++) {
|
|
start = update.start(i);
|
|
end = update.end(i);
|
|
|
|
if (edges.some(overlapsCurrentEnd)) {
|
|
continue;
|
|
}
|
|
|
|
// at this point it must be a unique non-shrinking end edge
|
|
result.push(end);
|
|
}
|
|
}
|
|
|
|
// we err on the side of caution and return null if didn't find
|
|
// exactly *one* differing end edge in the search above
|
|
if (result.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
return result[0];
|
|
};
|
|
|
|
/**
|
|
* Calculate the intersection of two TimeRanges
|
|
* @param {TimeRanges} bufferA
|
|
* @param {TimeRanges} bufferB
|
|
* @returns {TimeRanges} The interesection of `bufferA` with `bufferB`
|
|
*/
|
|
var bufferIntersection = function bufferIntersection(bufferA, bufferB) {
|
|
var start = null;
|
|
var end = null;
|
|
var arity = 0;
|
|
var extents = [];
|
|
var ranges = [];
|
|
|
|
if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
|
|
return _videoJs2['default'].createTimeRange();
|
|
}
|
|
|
|
// Handle the case where we have both buffers and create an
|
|
// intersection of the two
|
|
var count = bufferA.length;
|
|
|
|
// A) Gather up all start and end times
|
|
while (count--) {
|
|
extents.push({ time: bufferA.start(count), type: 'start' });
|
|
extents.push({ time: bufferA.end(count), type: 'end' });
|
|
}
|
|
count = bufferB.length;
|
|
while (count--) {
|
|
extents.push({ time: bufferB.start(count), type: 'start' });
|
|
extents.push({ time: bufferB.end(count), type: 'end' });
|
|
}
|
|
// B) Sort them by time
|
|
extents.sort(function (a, b) {
|
|
return a.time - b.time;
|
|
});
|
|
|
|
// C) Go along one by one incrementing arity for start and decrementing
|
|
// arity for ends
|
|
for (count = 0; count < extents.length; count++) {
|
|
if (extents[count].type === 'start') {
|
|
arity++;
|
|
|
|
// D) If arity is ever incremented to 2 we are entering an
|
|
// overlapping range
|
|
if (arity === 2) {
|
|
start = extents[count].time;
|
|
}
|
|
} else if (extents[count].type === 'end') {
|
|
arity--;
|
|
|
|
// E) If arity is ever decremented to 1 we leaving an
|
|
// overlapping range
|
|
if (arity === 1) {
|
|
end = extents[count].time;
|
|
}
|
|
}
|
|
|
|
// F) Record overlapping ranges
|
|
if (start !== null && end !== null) {
|
|
ranges.push([start, end]);
|
|
start = null;
|
|
end = null;
|
|
}
|
|
}
|
|
|
|
return _videoJs2['default'].createTimeRanges(ranges);
|
|
};
|
|
|
|
/**
|
|
* Calculates the percentage of `segmentRange` that overlaps the
|
|
* `buffered` time ranges.
|
|
* @param {TimeRanges} segmentRange - the time range that the segment
|
|
* covers adjusted according to currentTime
|
|
* @param {TimeRanges} referenceRange - the original time range that the
|
|
* segment covers
|
|
* @param {Number} currentTime - time in seconds where the current playback
|
|
* is at
|
|
* @param {TimeRanges} buffered - the currently buffered time ranges
|
|
* @returns {Number} percent of the segment currently buffered
|
|
*/
|
|
var calculateBufferedPercent = function calculateBufferedPercent(adjustedRange, referenceRange, currentTime, buffered) {
|
|
var referenceDuration = referenceRange.end(0) - referenceRange.start(0);
|
|
var adjustedDuration = adjustedRange.end(0) - adjustedRange.start(0);
|
|
var bufferMissingFromAdjusted = referenceDuration - adjustedDuration;
|
|
var adjustedIntersection = bufferIntersection(adjustedRange, buffered);
|
|
var referenceIntersection = bufferIntersection(referenceRange, buffered);
|
|
var adjustedOverlap = 0;
|
|
var referenceOverlap = 0;
|
|
|
|
var count = adjustedIntersection.length;
|
|
|
|
while (count--) {
|
|
adjustedOverlap += adjustedIntersection.end(count) - adjustedIntersection.start(count);
|
|
|
|
// If the current overlap segment starts at currentTime, then increase the
|
|
// overlap duration so that it actually starts at the beginning of referenceRange
|
|
// by including the difference between the two Range's durations
|
|
// This is a work around for the way Flash has no buffer before currentTime
|
|
if (adjustedIntersection.start(count) === currentTime) {
|
|
adjustedOverlap += bufferMissingFromAdjusted;
|
|
}
|
|
}
|
|
|
|
count = referenceIntersection.length;
|
|
|
|
while (count--) {
|
|
referenceOverlap += referenceIntersection.end(count) - referenceIntersection.start(count);
|
|
}
|
|
|
|
// Use whichever value is larger for the percentage-buffered since that value
|
|
// is likely more accurate because the only way
|
|
return Math.max(adjustedOverlap, referenceOverlap) / referenceDuration * 100;
|
|
};
|
|
|
|
/**
|
|
* Return the amount of a range specified by the startOfSegment and segmentDuration
|
|
* overlaps the current buffered content.
|
|
*
|
|
* @param {Number} startOfSegment - the time where the segment begins
|
|
* @param {Number} segmentDuration - the duration of the segment in seconds
|
|
* @param {Number} currentTime - time in seconds where the current playback
|
|
* is at
|
|
* @param {TimeRanges} buffered - the state of the buffer
|
|
* @returns {Number} percentage of the segment's time range that is
|
|
* already in `buffered`
|
|
*/
|
|
var getSegmentBufferedPercent = function getSegmentBufferedPercent(startOfSegment, segmentDuration, currentTime, buffered) {
|
|
var endOfSegment = startOfSegment + segmentDuration;
|
|
|
|
// The entire time range of the segment
|
|
var originalSegmentRange = _videoJs2['default'].createTimeRanges([[startOfSegment, endOfSegment]]);
|
|
|
|
// The adjusted segment time range that is setup such that it starts
|
|
// no earlier than currentTime
|
|
// Flash has no notion of a back-buffer so adjustedSegmentRange adjusts
|
|
// for that and the function will still return 100% if a only half of a
|
|
// segment is actually in the buffer as long as the currentTime is also
|
|
// half-way through the segment
|
|
var adjustedSegmentRange = _videoJs2['default'].createTimeRanges([[clamp(startOfSegment, [currentTime, endOfSegment]), endOfSegment]]);
|
|
|
|
// This condition happens when the currentTime is beyond the segment's
|
|
// end time
|
|
if (adjustedSegmentRange.start(0) === adjustedSegmentRange.end(0)) {
|
|
return 0;
|
|
}
|
|
|
|
var percent = calculateBufferedPercent(adjustedSegmentRange, originalSegmentRange, currentTime, buffered);
|
|
|
|
// If the segment is reported as having a zero duration, return 0%
|
|
// since it is likely that we will need to fetch the segment
|
|
if (isNaN(percent) || percent === Infinity || percent === -Infinity) {
|
|
return 0;
|
|
}
|
|
|
|
return percent;
|
|
};
|
|
|
|
/**
|
|
* Gets a human readable string for a TimeRange
|
|
*
|
|
* @param {TimeRange} range
|
|
* @returns {String} a human readable string
|
|
*/
|
|
var printableRange = function printableRange(range) {
|
|
var strArr = [];
|
|
|
|
if (!range || !range.length) {
|
|
return '';
|
|
}
|
|
|
|
for (var i = 0; i < range.length; i++) {
|
|
strArr.push(range.start(i) + ' => ' + range.end(i));
|
|
}
|
|
|
|
return strArr.join(', ');
|
|
};
|
|
|
|
/**
|
|
* Calculates the amount of time left in seconds until the player hits the end of the
|
|
* buffer and causes a rebuffer
|
|
*
|
|
* @param {TimeRange} buffered
|
|
* The state of the buffer
|
|
* @param {Numnber} currentTime
|
|
* The current time of the player
|
|
* @param {Number} playbackRate
|
|
* The current playback rate of the player. Defaults to 1.
|
|
* @return {Number}
|
|
* Time until the player has to start rebuffering in seconds.
|
|
* @function timeUntilRebuffer
|
|
*/
|
|
var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime) {
|
|
var playbackRate = arguments.length <= 2 || arguments[2] === undefined ? 1 : arguments[2];
|
|
|
|
var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
|
|
|
|
return (bufferedEnd - currentTime) / playbackRate;
|
|
};
|
|
|
|
exports['default'] = {
|
|
findRange: findRange,
|
|
findNextRange: findNextRange,
|
|
findGaps: findGaps,
|
|
findSoleUncommonTimeRangesEnd: findSoleUncommonTimeRangesEnd,
|
|
getSegmentBufferedPercent: getSegmentBufferedPercent,
|
|
TIME_FUDGE_FACTOR: TIME_FUDGE_FACTOR,
|
|
SAFE_TIME_DELTA: SAFE_TIME_DELTA,
|
|
printableRange: printableRange,
|
|
timeUntilRebuffer: timeUntilRebuffer
|
|
};
|
|
module.exports = exports['default']; |