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