ranges.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /**
  2. * ranges
  3. *
  4. * Utilities for working with TimeRanges.
  5. *
  6. */
  7. 'use strict';
  8. Object.defineProperty(exports, '__esModule', {
  9. value: true
  10. });
  11. 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'); } }; })();
  12. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
  13. var _videoJs = require('video.js');
  14. var _videoJs2 = _interopRequireDefault(_videoJs);
  15. // Fudge factor to account for TimeRanges rounding
  16. var TIME_FUDGE_FACTOR = 1 / 30;
  17. // Comparisons between time values such as current time and the end of the buffered range
  18. // can be misleading because of precision differences or when the current media has poorly
  19. // aligned audio and video, which can cause values to be slightly off from what you would
  20. // expect. This value is what we consider to be safe to use in such comparisons to account
  21. // for these scenarios.
  22. var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
  23. /**
  24. * Clamps a value to within a range
  25. * @param {Number} num - the value to clamp
  26. * @param {Number} start - the start of the range to clamp within, inclusive
  27. * @param {Number} end - the end of the range to clamp within, inclusive
  28. * @return {Number}
  29. */
  30. var clamp = function clamp(num, _ref) {
  31. var _ref2 = _slicedToArray(_ref, 2);
  32. var start = _ref2[0];
  33. var end = _ref2[1];
  34. return Math.min(Math.max(start, num), end);
  35. };
  36. var filterRanges = function filterRanges(timeRanges, predicate) {
  37. var results = [];
  38. var i = undefined;
  39. if (timeRanges && timeRanges.length) {
  40. // Search for ranges that match the predicate
  41. for (i = 0; i < timeRanges.length; i++) {
  42. if (predicate(timeRanges.start(i), timeRanges.end(i))) {
  43. results.push([timeRanges.start(i), timeRanges.end(i)]);
  44. }
  45. }
  46. }
  47. return _videoJs2['default'].createTimeRanges(results);
  48. };
  49. /**
  50. * Attempts to find the buffered TimeRange that contains the specified
  51. * time.
  52. * @param {TimeRanges} buffered - the TimeRanges object to query
  53. * @param {number} time - the time to filter on.
  54. * @returns {TimeRanges} a new TimeRanges object
  55. */
  56. var findRange = function findRange(buffered, time) {
  57. return filterRanges(buffered, function (start, end) {
  58. return start - TIME_FUDGE_FACTOR <= time && end + TIME_FUDGE_FACTOR >= time;
  59. });
  60. };
  61. /**
  62. * Returns the TimeRanges that begin later than the specified time.
  63. * @param {TimeRanges} timeRanges - the TimeRanges object to query
  64. * @param {number} time - the time to filter on.
  65. * @returns {TimeRanges} a new TimeRanges object.
  66. */
  67. var findNextRange = function findNextRange(timeRanges, time) {
  68. return filterRanges(timeRanges, function (start) {
  69. return start - TIME_FUDGE_FACTOR >= time;
  70. });
  71. };
  72. /**
  73. * Returns gaps within a list of TimeRanges
  74. * @param {TimeRanges} buffered - the TimeRanges object
  75. * @return {TimeRanges} a TimeRanges object of gaps
  76. */
  77. var findGaps = function findGaps(buffered) {
  78. if (buffered.length < 2) {
  79. return _videoJs2['default'].createTimeRanges();
  80. }
  81. var ranges = [];
  82. for (var i = 1; i < buffered.length; i++) {
  83. var start = buffered.end(i - 1);
  84. var end = buffered.start(i);
  85. ranges.push([start, end]);
  86. }
  87. return _videoJs2['default'].createTimeRanges(ranges);
  88. };
  89. /**
  90. * Search for a likely end time for the segment that was just appened
  91. * based on the state of the `buffered` property before and after the
  92. * append. If we fin only one such uncommon end-point return it.
  93. * @param {TimeRanges} original - the buffered time ranges before the update
  94. * @param {TimeRanges} update - the buffered time ranges after the update
  95. * @returns {Number|null} the end time added between `original` and `update`,
  96. * or null if one cannot be unambiguously determined.
  97. */
  98. var findSoleUncommonTimeRangesEnd = function findSoleUncommonTimeRangesEnd(original, update) {
  99. var i = undefined;
  100. var start = undefined;
  101. var end = undefined;
  102. var result = [];
  103. var edges = [];
  104. // In order to qualify as a possible candidate, the end point must:
  105. // 1) Not have already existed in the `original` ranges
  106. // 2) Not result from the shrinking of a range that already existed
  107. // in the `original` ranges
  108. // 3) Not be contained inside of a range that existed in `original`
  109. var overlapsCurrentEnd = function overlapsCurrentEnd(span) {
  110. return span[0] <= end && span[1] >= end;
  111. };
  112. if (original) {
  113. // Save all the edges in the `original` TimeRanges object
  114. for (i = 0; i < original.length; i++) {
  115. start = original.start(i);
  116. end = original.end(i);
  117. edges.push([start, end]);
  118. }
  119. }
  120. if (update) {
  121. // Save any end-points in `update` that are not in the `original`
  122. // TimeRanges object
  123. for (i = 0; i < update.length; i++) {
  124. start = update.start(i);
  125. end = update.end(i);
  126. if (edges.some(overlapsCurrentEnd)) {
  127. continue;
  128. }
  129. // at this point it must be a unique non-shrinking end edge
  130. result.push(end);
  131. }
  132. }
  133. // we err on the side of caution and return null if didn't find
  134. // exactly *one* differing end edge in the search above
  135. if (result.length !== 1) {
  136. return null;
  137. }
  138. return result[0];
  139. };
  140. /**
  141. * Calculate the intersection of two TimeRanges
  142. * @param {TimeRanges} bufferA
  143. * @param {TimeRanges} bufferB
  144. * @returns {TimeRanges} The interesection of `bufferA` with `bufferB`
  145. */
  146. var bufferIntersection = function bufferIntersection(bufferA, bufferB) {
  147. var start = null;
  148. var end = null;
  149. var arity = 0;
  150. var extents = [];
  151. var ranges = [];
  152. if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
  153. return _videoJs2['default'].createTimeRange();
  154. }
  155. // Handle the case where we have both buffers and create an
  156. // intersection of the two
  157. var count = bufferA.length;
  158. // A) Gather up all start and end times
  159. while (count--) {
  160. extents.push({ time: bufferA.start(count), type: 'start' });
  161. extents.push({ time: bufferA.end(count), type: 'end' });
  162. }
  163. count = bufferB.length;
  164. while (count--) {
  165. extents.push({ time: bufferB.start(count), type: 'start' });
  166. extents.push({ time: bufferB.end(count), type: 'end' });
  167. }
  168. // B) Sort them by time
  169. extents.sort(function (a, b) {
  170. return a.time - b.time;
  171. });
  172. // C) Go along one by one incrementing arity for start and decrementing
  173. // arity for ends
  174. for (count = 0; count < extents.length; count++) {
  175. if (extents[count].type === 'start') {
  176. arity++;
  177. // D) If arity is ever incremented to 2 we are entering an
  178. // overlapping range
  179. if (arity === 2) {
  180. start = extents[count].time;
  181. }
  182. } else if (extents[count].type === 'end') {
  183. arity--;
  184. // E) If arity is ever decremented to 1 we leaving an
  185. // overlapping range
  186. if (arity === 1) {
  187. end = extents[count].time;
  188. }
  189. }
  190. // F) Record overlapping ranges
  191. if (start !== null && end !== null) {
  192. ranges.push([start, end]);
  193. start = null;
  194. end = null;
  195. }
  196. }
  197. return _videoJs2['default'].createTimeRanges(ranges);
  198. };
  199. /**
  200. * Calculates the percentage of `segmentRange` that overlaps the
  201. * `buffered` time ranges.
  202. * @param {TimeRanges} segmentRange - the time range that the segment
  203. * covers adjusted according to currentTime
  204. * @param {TimeRanges} referenceRange - the original time range that the
  205. * segment covers
  206. * @param {Number} currentTime - time in seconds where the current playback
  207. * is at
  208. * @param {TimeRanges} buffered - the currently buffered time ranges
  209. * @returns {Number} percent of the segment currently buffered
  210. */
  211. var calculateBufferedPercent = function calculateBufferedPercent(adjustedRange, referenceRange, currentTime, buffered) {
  212. var referenceDuration = referenceRange.end(0) - referenceRange.start(0);
  213. var adjustedDuration = adjustedRange.end(0) - adjustedRange.start(0);
  214. var bufferMissingFromAdjusted = referenceDuration - adjustedDuration;
  215. var adjustedIntersection = bufferIntersection(adjustedRange, buffered);
  216. var referenceIntersection = bufferIntersection(referenceRange, buffered);
  217. var adjustedOverlap = 0;
  218. var referenceOverlap = 0;
  219. var count = adjustedIntersection.length;
  220. while (count--) {
  221. adjustedOverlap += adjustedIntersection.end(count) - adjustedIntersection.start(count);
  222. // If the current overlap segment starts at currentTime, then increase the
  223. // overlap duration so that it actually starts at the beginning of referenceRange
  224. // by including the difference between the two Range's durations
  225. // This is a work around for the way Flash has no buffer before currentTime
  226. if (adjustedIntersection.start(count) === currentTime) {
  227. adjustedOverlap += bufferMissingFromAdjusted;
  228. }
  229. }
  230. count = referenceIntersection.length;
  231. while (count--) {
  232. referenceOverlap += referenceIntersection.end(count) - referenceIntersection.start(count);
  233. }
  234. // Use whichever value is larger for the percentage-buffered since that value
  235. // is likely more accurate because the only way
  236. return Math.max(adjustedOverlap, referenceOverlap) / referenceDuration * 100;
  237. };
  238. /**
  239. * Return the amount of a range specified by the startOfSegment and segmentDuration
  240. * overlaps the current buffered content.
  241. *
  242. * @param {Number} startOfSegment - the time where the segment begins
  243. * @param {Number} segmentDuration - the duration of the segment in seconds
  244. * @param {Number} currentTime - time in seconds where the current playback
  245. * is at
  246. * @param {TimeRanges} buffered - the state of the buffer
  247. * @returns {Number} percentage of the segment's time range that is
  248. * already in `buffered`
  249. */
  250. var getSegmentBufferedPercent = function getSegmentBufferedPercent(startOfSegment, segmentDuration, currentTime, buffered) {
  251. var endOfSegment = startOfSegment + segmentDuration;
  252. // The entire time range of the segment
  253. var originalSegmentRange = _videoJs2['default'].createTimeRanges([[startOfSegment, endOfSegment]]);
  254. // The adjusted segment time range that is setup such that it starts
  255. // no earlier than currentTime
  256. // Flash has no notion of a back-buffer so adjustedSegmentRange adjusts
  257. // for that and the function will still return 100% if a only half of a
  258. // segment is actually in the buffer as long as the currentTime is also
  259. // half-way through the segment
  260. var adjustedSegmentRange = _videoJs2['default'].createTimeRanges([[clamp(startOfSegment, [currentTime, endOfSegment]), endOfSegment]]);
  261. // This condition happens when the currentTime is beyond the segment's
  262. // end time
  263. if (adjustedSegmentRange.start(0) === adjustedSegmentRange.end(0)) {
  264. return 0;
  265. }
  266. var percent = calculateBufferedPercent(adjustedSegmentRange, originalSegmentRange, currentTime, buffered);
  267. // If the segment is reported as having a zero duration, return 0%
  268. // since it is likely that we will need to fetch the segment
  269. if (isNaN(percent) || percent === Infinity || percent === -Infinity) {
  270. return 0;
  271. }
  272. return percent;
  273. };
  274. /**
  275. * Gets a human readable string for a TimeRange
  276. *
  277. * @param {TimeRange} range
  278. * @returns {String} a human readable string
  279. */
  280. var printableRange = function printableRange(range) {
  281. var strArr = [];
  282. if (!range || !range.length) {
  283. return '';
  284. }
  285. for (var i = 0; i < range.length; i++) {
  286. strArr.push(range.start(i) + ' => ' + range.end(i));
  287. }
  288. return strArr.join(', ');
  289. };
  290. /**
  291. * Calculates the amount of time left in seconds until the player hits the end of the
  292. * buffer and causes a rebuffer
  293. *
  294. * @param {TimeRange} buffered
  295. * The state of the buffer
  296. * @param {Numnber} currentTime
  297. * The current time of the player
  298. * @param {Number} playbackRate
  299. * The current playback rate of the player. Defaults to 1.
  300. * @return {Number}
  301. * Time until the player has to start rebuffering in seconds.
  302. * @function timeUntilRebuffer
  303. */
  304. var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime) {
  305. var playbackRate = arguments.length <= 2 || arguments[2] === undefined ? 1 : arguments[2];
  306. var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
  307. return (bufferedEnd - currentTime) / playbackRate;
  308. };
  309. exports['default'] = {
  310. findRange: findRange,
  311. findNextRange: findNextRange,
  312. findGaps: findGaps,
  313. findSoleUncommonTimeRangesEnd: findSoleUncommonTimeRangesEnd,
  314. getSegmentBufferedPercent: getSegmentBufferedPercent,
  315. TIME_FUDGE_FACTOR: TIME_FUDGE_FACTOR,
  316. SAFE_TIME_DELTA: SAFE_TIME_DELTA,
  317. printableRange: printableRange,
  318. timeUntilRebuffer: timeUntilRebuffer
  319. };
  320. module.exports = exports['default'];