UNPKG

11.5 kBJavaScriptView Raw
1/**
2 * ranges
3 *
4 * Utilities for working with TimeRanges.
5 *
6 */
7
8import videojs from 'video.js';
9
10// Fudge factor to account for TimeRanges rounding
11const 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 */
20const clamp = function(num, [start, end]) {
21 return Math.min(Math.max(start, num), end);
22};
23const 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 */
46const 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 */
59const 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 */
70const 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 */
96const 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 */
153const 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 */
227const 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 */
278const 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 */
327const 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 */
355const 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
361export default {
362 findRange,
363 findNextRange,
364 findGaps,
365 findSoleUncommonTimeRangesEnd,
366 getSegmentBufferedPercent,
367 TIME_FUDGE_FACTOR,
368 printableRange,
369 timeUntilRebuffer
370};