1 | /**
|
2 | * ranges
|
3 | *
|
4 | * Utilities for working with TimeRanges.
|
5 | *
|
6 | */
|
7 |
|
8 | import videojs from 'video.js';
|
9 |
|
10 | // Fudge factor to account for TimeRanges rounding
|
11 | export const TIME_FUDGE_FACTOR = 1 / 30;
|
12 | // Comparisons between time values such as current time and the end of the buffered range
|
13 | // can be misleading because of precision differences or when the current media has poorly
|
14 | // aligned audio and video, which can cause values to be slightly off from what you would
|
15 | // expect. This value is what we consider to be safe to use in such comparisons to account
|
16 | // for these scenarios.
|
17 | export const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
|
18 |
|
19 | /**
|
20 | * Clamps a value to within a range
|
21 | *
|
22 | * @param {number} num - the value to clamp
|
23 | * @param {number} start - the start of the range to clamp within, inclusive
|
24 | * @param {number} end - the end of the range to clamp within, inclusive
|
25 | * @return {number}
|
26 | */
|
27 | const clamp = function(num, [start, end]) {
|
28 | return Math.min(Math.max(start, num), end);
|
29 | };
|
30 | const filterRanges = function(timeRanges, predicate) {
|
31 | const results = [];
|
32 | let i;
|
33 |
|
34 | if (timeRanges && timeRanges.length) {
|
35 | // Search for ranges that match the predicate
|
36 | for (i = 0; i < timeRanges.length; i++) {
|
37 | if (predicate(timeRanges.start(i), timeRanges.end(i))) {
|
38 | results.push([timeRanges.start(i), timeRanges.end(i)]);
|
39 | }
|
40 | }
|
41 | }
|
42 |
|
43 | return videojs.createTimeRanges(results);
|
44 | };
|
45 |
|
46 | /**
|
47 | * Attempts to find the buffered TimeRange that contains the specified
|
48 | * time.
|
49 | *
|
50 | * @param {TimeRanges} buffered - the TimeRanges object to query
|
51 | * @param {number} time - the time to filter on.
|
52 | * @return {TimeRanges} a new TimeRanges object
|
53 | */
|
54 | export const findRange = function(buffered, time) {
|
55 | return filterRanges(buffered, function(start, end) {
|
56 | return start - SAFE_TIME_DELTA <= time &&
|
57 | end + SAFE_TIME_DELTA >= time;
|
58 | });
|
59 | };
|
60 |
|
61 | /**
|
62 | * Returns the TimeRanges that begin later than the specified time.
|
63 | *
|
64 | * @param {TimeRanges} timeRanges - the TimeRanges object to query
|
65 | * @param {number} time - the time to filter on.
|
66 | * @return {TimeRanges} a new TimeRanges object.
|
67 | */
|
68 | export const findNextRange = function(timeRanges, time) {
|
69 | return filterRanges(timeRanges, function(start) {
|
70 | return start - TIME_FUDGE_FACTOR >= time;
|
71 | });
|
72 | };
|
73 |
|
74 | /**
|
75 | * Returns gaps within a list of TimeRanges
|
76 | *
|
77 | * @param {TimeRanges} buffered - the TimeRanges object
|
78 | * @return {TimeRanges} a TimeRanges object of gaps
|
79 | */
|
80 | export const findGaps = function(buffered) {
|
81 | if (buffered.length < 2) {
|
82 | return videojs.createTimeRanges();
|
83 | }
|
84 |
|
85 | const ranges = [];
|
86 |
|
87 | for (let i = 1; i < buffered.length; i++) {
|
88 | const start = buffered.end(i - 1);
|
89 | const end = buffered.start(i);
|
90 |
|
91 | ranges.push([start, end]);
|
92 | }
|
93 |
|
94 | return videojs.createTimeRanges(ranges);
|
95 | };
|
96 |
|
97 | /**
|
98 | * Search for a likely end time for the segment that was just appened
|
99 | * based on the state of the `buffered` property before and after the
|
100 | * append. If we fin only one such uncommon end-point return it.
|
101 | *
|
102 | * @param {TimeRanges} original - the buffered time ranges before the update
|
103 | * @param {TimeRanges} update - the buffered time ranges after the update
|
104 | * @return {number|null} the end time added between `original` and `update`,
|
105 | * or null if one cannot be unambiguously determined.
|
106 | */
|
107 | export const findSoleUncommonTimeRangesEnd = function(original, update) {
|
108 | let i;
|
109 | let start;
|
110 | let end;
|
111 | const result = [];
|
112 | const edges = [];
|
113 |
|
114 | // In order to qualify as a possible candidate, the end point must:
|
115 | // 1) Not have already existed in the `original` ranges
|
116 | // 2) Not result from the shrinking of a range that already existed
|
117 | // in the `original` ranges
|
118 | // 3) Not be contained inside of a range that existed in `original`
|
119 | const overlapsCurrentEnd = function(span) {
|
120 | return (span[0] <= end && span[1] >= end);
|
121 | };
|
122 |
|
123 | if (original) {
|
124 | // Save all the edges in the `original` TimeRanges object
|
125 | for (i = 0; i < original.length; i++) {
|
126 | start = original.start(i);
|
127 | end = original.end(i);
|
128 |
|
129 | edges.push([start, end]);
|
130 | }
|
131 | }
|
132 |
|
133 | if (update) {
|
134 | // Save any end-points in `update` that are not in the `original`
|
135 | // TimeRanges object
|
136 | for (i = 0; i < update.length; i++) {
|
137 | start = update.start(i);
|
138 | end = update.end(i);
|
139 |
|
140 | if (edges.some(overlapsCurrentEnd)) {
|
141 | continue;
|
142 | }
|
143 |
|
144 | // at this point it must be a unique non-shrinking end edge
|
145 | result.push(end);
|
146 | }
|
147 | }
|
148 |
|
149 | // we err on the side of caution and return null if didn't find
|
150 | // exactly *one* differing end edge in the search above
|
151 | if (result.length !== 1) {
|
152 | return null;
|
153 | }
|
154 |
|
155 | return result[0];
|
156 | };
|
157 |
|
158 | /**
|
159 | * Calculate the intersection of two TimeRanges
|
160 | *
|
161 | * @param {TimeRanges} bufferA
|
162 | * @param {TimeRanges} bufferB
|
163 | * @return {TimeRanges} The interesection of `bufferA` with `bufferB`
|
164 | */
|
165 | export const bufferIntersection = function(bufferA, bufferB) {
|
166 | let start = null;
|
167 | let end = null;
|
168 | let arity = 0;
|
169 | const extents = [];
|
170 | const ranges = [];
|
171 |
|
172 | if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
|
173 | return videojs.createTimeRange();
|
174 | }
|
175 |
|
176 | // Handle the case where we have both buffers and create an
|
177 | // intersection of the two
|
178 | let count = bufferA.length;
|
179 |
|
180 | // A) Gather up all start and end times
|
181 | while (count--) {
|
182 | extents.push({time: bufferA.start(count), type: 'start'});
|
183 | extents.push({time: bufferA.end(count), type: 'end'});
|
184 | }
|
185 | count = bufferB.length;
|
186 | while (count--) {
|
187 | extents.push({time: bufferB.start(count), type: 'start'});
|
188 | extents.push({time: bufferB.end(count), type: 'end'});
|
189 | }
|
190 | // B) Sort them by time
|
191 | extents.sort(function(a, b) {
|
192 | return a.time - b.time;
|
193 | });
|
194 |
|
195 | // C) Go along one by one incrementing arity for start and decrementing
|
196 | // arity for ends
|
197 | for (count = 0; count < extents.length; count++) {
|
198 | if (extents[count].type === 'start') {
|
199 | arity++;
|
200 |
|
201 | // D) If arity is ever incremented to 2 we are entering an
|
202 | // overlapping range
|
203 | if (arity === 2) {
|
204 | start = extents[count].time;
|
205 | }
|
206 | } else if (extents[count].type === 'end') {
|
207 | arity--;
|
208 |
|
209 | // E) If arity is ever decremented to 1 we leaving an
|
210 | // overlapping range
|
211 | if (arity === 1) {
|
212 | end = extents[count].time;
|
213 | }
|
214 | }
|
215 |
|
216 | // F) Record overlapping ranges
|
217 | if (start !== null && end !== null) {
|
218 | ranges.push([start, end]);
|
219 | start = null;
|
220 | end = null;
|
221 | }
|
222 | }
|
223 |
|
224 | return videojs.createTimeRanges(ranges);
|
225 | };
|
226 |
|
227 | /**
|
228 | * Calculates the percentage of `segmentRange` that overlaps the
|
229 | * `buffered` time ranges.
|
230 | *
|
231 | * @param {TimeRanges} segmentRange - the time range that the segment
|
232 | * covers adjusted according to currentTime
|
233 | * @param {TimeRanges} referenceRange - the original time range that the
|
234 | * segment covers
|
235 | * @param {number} currentTime - time in seconds where the current playback
|
236 | * is at
|
237 | * @param {TimeRanges} buffered - the currently buffered time ranges
|
238 | * @return {number} percent of the segment currently buffered
|
239 | */
|
240 | const calculateBufferedPercent = function(
|
241 | adjustedRange,
|
242 | referenceRange,
|
243 | currentTime,
|
244 | buffered
|
245 | ) {
|
246 | const referenceDuration = referenceRange.end(0) - referenceRange.start(0);
|
247 | const adjustedDuration = adjustedRange.end(0) - adjustedRange.start(0);
|
248 | const bufferMissingFromAdjusted = referenceDuration - adjustedDuration;
|
249 | const adjustedIntersection = bufferIntersection(adjustedRange, buffered);
|
250 | const referenceIntersection = bufferIntersection(referenceRange, buffered);
|
251 | let adjustedOverlap = 0;
|
252 | let referenceOverlap = 0;
|
253 |
|
254 | let count = adjustedIntersection.length;
|
255 |
|
256 | while (count--) {
|
257 | adjustedOverlap += adjustedIntersection.end(count) -
|
258 | adjustedIntersection.start(count);
|
259 |
|
260 | // If the current overlap segment starts at currentTime, then increase the
|
261 | // overlap duration so that it actually starts at the beginning of referenceRange
|
262 | // by including the difference between the two Range's durations
|
263 | // This is a work around for the way Flash has no buffer before currentTime
|
264 | // TODO: see if this is still necessary since Flash isn't included
|
265 | if (adjustedIntersection.start(count) === currentTime) {
|
266 | adjustedOverlap += bufferMissingFromAdjusted;
|
267 | }
|
268 | }
|
269 |
|
270 | count = referenceIntersection.length;
|
271 |
|
272 | while (count--) {
|
273 | referenceOverlap += referenceIntersection.end(count) -
|
274 | referenceIntersection.start(count);
|
275 | }
|
276 |
|
277 | // Use whichever value is larger for the percentage-buffered since that value
|
278 | // is likely more accurate because the only way
|
279 | return Math.max(adjustedOverlap, referenceOverlap) / referenceDuration * 100;
|
280 | };
|
281 |
|
282 | /**
|
283 | * Return the amount of a range specified by the startOfSegment and segmentDuration
|
284 | * overlaps the current buffered content.
|
285 | *
|
286 | * @param {number} startOfSegment - the time where the segment begins
|
287 | * @param {number} segmentDuration - the duration of the segment in seconds
|
288 | * @param {number} currentTime - time in seconds where the current playback
|
289 | * is at
|
290 | * @param {TimeRanges} buffered - the state of the buffer
|
291 | * @return {number} percentage of the segment's time range that is
|
292 | * already in `buffered`
|
293 | */
|
294 | export const getSegmentBufferedPercent = function(
|
295 | startOfSegment,
|
296 | segmentDuration,
|
297 | currentTime,
|
298 | buffered
|
299 | ) {
|
300 | const endOfSegment = startOfSegment + segmentDuration;
|
301 |
|
302 | // The entire time range of the segment
|
303 | const originalSegmentRange = videojs.createTimeRanges([[
|
304 | startOfSegment,
|
305 | endOfSegment
|
306 | ]]);
|
307 |
|
308 | // The adjusted segment time range that is setup such that it starts
|
309 | // no earlier than currentTime
|
310 | // Flash has no notion of a back-buffer so adjustedSegmentRange adjusts
|
311 | // for that and the function will still return 100% if a only half of a
|
312 | // segment is actually in the buffer as long as the currentTime is also
|
313 | // half-way through the segment
|
314 | const adjustedSegmentRange = videojs.createTimeRanges([[
|
315 | clamp(startOfSegment, [currentTime, endOfSegment]),
|
316 | endOfSegment
|
317 | ]]);
|
318 |
|
319 | // This condition happens when the currentTime is beyond the segment's
|
320 | // end time
|
321 | if (adjustedSegmentRange.start(0) === adjustedSegmentRange.end(0)) {
|
322 | return 0;
|
323 | }
|
324 |
|
325 | const percent = calculateBufferedPercent(
|
326 | adjustedSegmentRange,
|
327 | originalSegmentRange,
|
328 | currentTime,
|
329 | buffered
|
330 | );
|
331 |
|
332 | // If the segment is reported as having a zero duration, return 0%
|
333 | // since it is likely that we will need to fetch the segment
|
334 | if (isNaN(percent) || percent === Infinity || percent === -Infinity) {
|
335 | return 0;
|
336 | }
|
337 |
|
338 | return percent;
|
339 | };
|
340 |
|
341 | /**
|
342 | * Gets a human readable string for a TimeRange
|
343 | *
|
344 | * @param {TimeRange} range
|
345 | * @return {string} a human readable string
|
346 | */
|
347 | export const printableRange = (range) => {
|
348 | const strArr = [];
|
349 |
|
350 | if (!range || !range.length) {
|
351 | return '';
|
352 | }
|
353 |
|
354 | for (let i = 0; i < range.length; i++) {
|
355 | strArr.push(range.start(i) + ' => ' + range.end(i));
|
356 | }
|
357 |
|
358 | return strArr.join(', ');
|
359 | };
|
360 |
|
361 | /**
|
362 | * Calculates the amount of time left in seconds until the player hits the end of the
|
363 | * buffer and causes a rebuffer
|
364 | *
|
365 | * @param {TimeRange} buffered
|
366 | * The state of the buffer
|
367 | * @param {Numnber} currentTime
|
368 | * The current time of the player
|
369 | * @param {number} playbackRate
|
370 | * The current playback rate of the player. Defaults to 1.
|
371 | * @return {number}
|
372 | * Time until the player has to start rebuffering in seconds.
|
373 | * @function timeUntilRebuffer
|
374 | */
|
375 | export const timeUntilRebuffer = function(buffered, currentTime, playbackRate = 1) {
|
376 | const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
|
377 |
|
378 | return (bufferedEnd - currentTime) / playbackRate;
|
379 | };
|
380 |
|
381 | /**
|
382 | * Converts a TimeRanges object into an array representation
|
383 | *
|
384 | * @param {TimeRanges} timeRanges
|
385 | * @return {Array}
|
386 | */
|
387 | export const timeRangesToArray = (timeRanges) => {
|
388 | const timeRangesList = [];
|
389 |
|
390 | for (let i = 0; i < timeRanges.length; i++) {
|
391 | timeRangesList.push({
|
392 | start: timeRanges.start(i),
|
393 | end: timeRanges.end(i)
|
394 | });
|
395 | }
|
396 |
|
397 | return timeRangesList;
|
398 | };
|
399 |
|
400 | /**
|
401 | * Determines if two time range objects are different.
|
402 | *
|
403 | * @param {TimeRange} a
|
404 | * the first time range object to check
|
405 | *
|
406 | * @param {TimeRange} b
|
407 | * the second time range object to check
|
408 | *
|
409 | * @return {Boolean}
|
410 | * Whether the time range objects differ
|
411 | */
|
412 |
|
413 | export const isRangeDifferent = function(a, b) {
|
414 | // same object
|
415 | if (a === b) {
|
416 | return false;
|
417 | }
|
418 |
|
419 | // one or the other is undefined
|
420 | if (!a && b || (!b && a)) {
|
421 | return true;
|
422 | }
|
423 |
|
424 | // length is different
|
425 | if (a.length !== b.length) {
|
426 | return true;
|
427 | }
|
428 |
|
429 | // see if any start/end pair is different
|
430 | for (let i = 0; i < a.length; i++) {
|
431 | if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) {
|
432 | return true;
|
433 | }
|
434 | }
|
435 |
|
436 | // if the length and every pair is the same
|
437 | // this is the same time range
|
438 | return false;
|
439 | };
|
440 |
|
441 | export const lastBufferedEnd = function(a) {
|
442 | if (!a || !a.length || !a.end) {
|
443 | return;
|
444 | }
|
445 |
|
446 | return a.end(a.length - 1);
|
447 | };
|
448 |
|
449 | /**
|
450 | * A utility function to add up the amount of time in a timeRange
|
451 | * after a specified startTime.
|
452 | * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0
|
453 | * would return 40 as there are 40s seconds after 0 in the timeRange
|
454 | *
|
455 | * @param {TimeRange} range
|
456 | * The range to check against
|
457 | * @param {number} startTime
|
458 | * The time in the time range that you should start counting from
|
459 | *
|
460 | * @return {number}
|
461 | * The number of seconds in the buffer passed the specified time.
|
462 | */
|
463 | export const timeAheadOf = function(range, startTime) {
|
464 | let time = 0;
|
465 |
|
466 | if (!range || !range.length) {
|
467 | return time;
|
468 | }
|
469 |
|
470 | for (let i = 0; i < range.length; i++) {
|
471 | const start = range.start(i);
|
472 | const end = range.end(i);
|
473 |
|
474 | // startTime is after this range entirely
|
475 | if (startTime > end) {
|
476 | continue;
|
477 | }
|
478 |
|
479 | // startTime is within this range
|
480 | if (startTime > start && startTime <= end) {
|
481 | time += end - startTime;
|
482 | continue;
|
483 | }
|
484 |
|
485 | // startTime is before this range.
|
486 | time += end - start;
|
487 | }
|
488 |
|
489 | return time;
|
490 | };
|