UNPKG

12 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// 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.
17const 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 */
26const clamp = function(num, [start, end]) {
27 return Math.min(Math.max(start, num), end);
28};
29const 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 */
52const 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 */
65const 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 */
76const 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 */
102const 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 */
159const 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 */
233const 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 */
284const 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 */
333const 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 */
361const 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
367export default {
368 findRange,
369 findNextRange,
370 findGaps,
371 findSoleUncommonTimeRangesEnd,
372 getSegmentBufferedPercent,
373 TIME_FUDGE_FACTOR,
374 SAFE_TIME_DELTA,
375 printableRange,
376 timeUntilRebuffer
377};