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