1 | /**
|
2 | * @file playlist.js
|
3 | *
|
4 | * Playlist related utilities.
|
5 | */
|
6 | ;
|
7 |
|
8 | Object.defineProperty(exports, '__esModule', {
|
9 | value: true
|
10 | });
|
11 |
|
12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
13 |
|
14 | var _videoJs = require('video.js');
|
15 |
|
16 | var _globalWindow = require('global/window');
|
17 |
|
18 | var _globalWindow2 = _interopRequireDefault(_globalWindow);
|
19 |
|
20 | /**
|
21 | * walk backward until we find a duration we can use
|
22 | * or return a failure
|
23 | *
|
24 | * @param {Playlist} playlist the playlist to walk through
|
25 | * @param {Number} endSequence the mediaSequence to stop walking on
|
26 | */
|
27 |
|
28 | var backwardDuration = function backwardDuration(playlist, endSequence) {
|
29 | var result = 0;
|
30 | var i = endSequence - playlist.mediaSequence;
|
31 | // if a start time is available for segment immediately following
|
32 | // the interval, use it
|
33 | var segment = playlist.segments[i];
|
34 |
|
35 | // Walk backward until we find the latest segment with timeline
|
36 | // information that is earlier than endSequence
|
37 | if (segment) {
|
38 | if (typeof segment.start !== 'undefined') {
|
39 | return { result: segment.start, precise: true };
|
40 | }
|
41 | if (typeof segment.end !== 'undefined') {
|
42 | return {
|
43 | result: segment.end - segment.duration,
|
44 | precise: true
|
45 | };
|
46 | }
|
47 | }
|
48 | while (i--) {
|
49 | segment = playlist.segments[i];
|
50 | if (typeof segment.end !== 'undefined') {
|
51 | return { result: result + segment.end, precise: true };
|
52 | }
|
53 |
|
54 | result += segment.duration;
|
55 |
|
56 | if (typeof segment.start !== 'undefined') {
|
57 | return { result: result + segment.start, precise: true };
|
58 | }
|
59 | }
|
60 | return { result: result, precise: false };
|
61 | };
|
62 |
|
63 | /**
|
64 | * walk forward until we find a duration we can use
|
65 | * or return a failure
|
66 | *
|
67 | * @param {Playlist} playlist the playlist to walk through
|
68 | * @param {Number} endSequence the mediaSequence to stop walking on
|
69 | */
|
70 | var forwardDuration = function forwardDuration(playlist, endSequence) {
|
71 | var result = 0;
|
72 | var segment = undefined;
|
73 | var i = endSequence - playlist.mediaSequence;
|
74 | // Walk forward until we find the earliest segment with timeline
|
75 | // information
|
76 |
|
77 | for (; i < playlist.segments.length; i++) {
|
78 | segment = playlist.segments[i];
|
79 | if (typeof segment.start !== 'undefined') {
|
80 | return {
|
81 | result: segment.start - result,
|
82 | precise: true
|
83 | };
|
84 | }
|
85 |
|
86 | result += segment.duration;
|
87 |
|
88 | if (typeof segment.end !== 'undefined') {
|
89 | return {
|
90 | result: segment.end - result,
|
91 | precise: true
|
92 | };
|
93 | }
|
94 | }
|
95 | // indicate we didn't find a useful duration estimate
|
96 | return { result: -1, precise: false };
|
97 | };
|
98 |
|
99 | /**
|
100 | * Calculate the media duration from the segments associated with a
|
101 | * playlist. The duration of a subinterval of the available segments
|
102 | * may be calculated by specifying an end index.
|
103 | *
|
104 | * @param {Object} playlist a media playlist object
|
105 | * @param {Number=} endSequence an exclusive upper boundary
|
106 | * for the playlist. Defaults to playlist length.
|
107 | * @param {Number} expired the amount of time that has dropped
|
108 | * off the front of the playlist in a live scenario
|
109 | * @return {Number} the duration between the first available segment
|
110 | * and end index.
|
111 | */
|
112 | var intervalDuration = function intervalDuration(playlist, endSequence, expired) {
|
113 | var backward = undefined;
|
114 | var forward = undefined;
|
115 |
|
116 | if (typeof endSequence === 'undefined') {
|
117 | endSequence = playlist.mediaSequence + playlist.segments.length;
|
118 | }
|
119 |
|
120 | if (endSequence < playlist.mediaSequence) {
|
121 | return 0;
|
122 | }
|
123 |
|
124 | // do a backward walk to estimate the duration
|
125 | backward = backwardDuration(playlist, endSequence);
|
126 | if (backward.precise) {
|
127 | // if we were able to base our duration estimate on timing
|
128 | // information provided directly from the Media Source, return
|
129 | // it
|
130 | return backward.result;
|
131 | }
|
132 |
|
133 | // walk forward to see if a precise duration estimate can be made
|
134 | // that way
|
135 | forward = forwardDuration(playlist, endSequence);
|
136 | if (forward.precise) {
|
137 | // we found a segment that has been buffered and so it's
|
138 | // position is known precisely
|
139 | return forward.result;
|
140 | }
|
141 |
|
142 | // return the less-precise, playlist-based duration estimate
|
143 | return backward.result + expired;
|
144 | };
|
145 |
|
146 | /**
|
147 | * Calculates the duration of a playlist. If a start and end index
|
148 | * are specified, the duration will be for the subset of the media
|
149 | * timeline between those two indices. The total duration for live
|
150 | * playlists is always Infinity.
|
151 | *
|
152 | * @param {Object} playlist a media playlist object
|
153 | * @param {Number=} endSequence an exclusive upper
|
154 | * boundary for the playlist. Defaults to the playlist media
|
155 | * sequence number plus its length.
|
156 | * @param {Number=} expired the amount of time that has
|
157 | * dropped off the front of the playlist in a live scenario
|
158 | * @return {Number} the duration between the start index and end
|
159 | * index.
|
160 | */
|
161 | var duration = function duration(playlist, endSequence, expired) {
|
162 | if (!playlist) {
|
163 | return 0;
|
164 | }
|
165 |
|
166 | if (typeof expired !== 'number') {
|
167 | expired = 0;
|
168 | }
|
169 |
|
170 | // if a slice of the total duration is not requested, use
|
171 | // playlist-level duration indicators when they're present
|
172 | if (typeof endSequence === 'undefined') {
|
173 | // if present, use the duration specified in the playlist
|
174 | if (playlist.totalDuration) {
|
175 | return playlist.totalDuration;
|
176 | }
|
177 |
|
178 | // duration should be Infinity for live playlists
|
179 | if (!playlist.endList) {
|
180 | return _globalWindow2['default'].Infinity;
|
181 | }
|
182 | }
|
183 |
|
184 | // calculate the total duration based on the segment durations
|
185 | return intervalDuration(playlist, endSequence, expired);
|
186 | };
|
187 |
|
188 | exports.duration = duration;
|
189 | /**
|
190 | * Calculate the time between two indexes in the current playlist
|
191 | * neight the start- nor the end-index need to be within the current
|
192 | * playlist in which case, the targetDuration of the playlist is used
|
193 | * to approximate the durations of the segments
|
194 | *
|
195 | * @param {Object} playlist a media playlist object
|
196 | * @param {Number} startIndex
|
197 | * @param {Number} endIndex
|
198 | * @return {Number} the number of seconds between startIndex and endIndex
|
199 | */
|
200 | var sumDurations = function sumDurations(playlist, startIndex, endIndex) {
|
201 | var durations = 0;
|
202 |
|
203 | if (startIndex > endIndex) {
|
204 | var _ref = [endIndex, startIndex];
|
205 | startIndex = _ref[0];
|
206 | endIndex = _ref[1];
|
207 | }
|
208 |
|
209 | if (startIndex < 0) {
|
210 | for (var i = startIndex; i < Math.min(0, endIndex); i++) {
|
211 | durations += playlist.targetDuration;
|
212 | }
|
213 | startIndex = 0;
|
214 | }
|
215 |
|
216 | for (var i = startIndex; i < endIndex; i++) {
|
217 | durations += playlist.segments[i].duration;
|
218 | }
|
219 |
|
220 | return durations;
|
221 | };
|
222 |
|
223 | exports.sumDurations = sumDurations;
|
224 | /**
|
225 | * Determines the media index of the segment corresponding to the safe edge of the live
|
226 | * window which is the duration of the last segment plus 2 target durations from the end
|
227 | * of the playlist.
|
228 | *
|
229 | * @param {Object} playlist
|
230 | * a media playlist object
|
231 | * @return {Number}
|
232 | * The media index of the segment at the safe live point. 0 if there is no "safe"
|
233 | * point.
|
234 | * @function safeLiveIndex
|
235 | */
|
236 | var safeLiveIndex = function safeLiveIndex(playlist) {
|
237 | if (!playlist.segments.length) {
|
238 | return 0;
|
239 | }
|
240 |
|
241 | var i = playlist.segments.length - 1;
|
242 | var distanceFromEnd = playlist.segments[i].duration || playlist.targetDuration;
|
243 | var safeDistance = distanceFromEnd + playlist.targetDuration * 2;
|
244 |
|
245 | while (i--) {
|
246 | distanceFromEnd += playlist.segments[i].duration;
|
247 |
|
248 | if (distanceFromEnd >= safeDistance) {
|
249 | break;
|
250 | }
|
251 | }
|
252 |
|
253 | return Math.max(0, i);
|
254 | };
|
255 |
|
256 | exports.safeLiveIndex = safeLiveIndex;
|
257 | /**
|
258 | * Calculates the playlist end time
|
259 | *
|
260 | * @param {Object} playlist a media playlist object
|
261 | * @param {Number=} expired the amount of time that has
|
262 | * dropped off the front of the playlist in a live scenario
|
263 | * @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the
|
264 | * playlist end calculation should consider the safe live end
|
265 | * (truncate the playlist end by three segments). This is normally
|
266 | * used for calculating the end of the playlist's seekable range.
|
267 | * @returns {Number} the end time of playlist
|
268 | * @function playlistEnd
|
269 | */
|
270 | var playlistEnd = function playlistEnd(playlist, expired, useSafeLiveEnd) {
|
271 | if (!playlist || !playlist.segments) {
|
272 | return null;
|
273 | }
|
274 | if (playlist.endList) {
|
275 | return duration(playlist);
|
276 | }
|
277 |
|
278 | if (expired === null) {
|
279 | return null;
|
280 | }
|
281 |
|
282 | expired = expired || 0;
|
283 |
|
284 | var endSequence = useSafeLiveEnd ? safeLiveIndex(playlist) : playlist.segments.length;
|
285 |
|
286 | return intervalDuration(playlist, playlist.mediaSequence + endSequence, expired);
|
287 | };
|
288 |
|
289 | exports.playlistEnd = playlistEnd;
|
290 | /**
|
291 | * Calculates the interval of time that is currently seekable in a
|
292 | * playlist. The returned time ranges are relative to the earliest
|
293 | * moment in the specified playlist that is still available. A full
|
294 | * seekable implementation for live streams would need to offset
|
295 | * these values by the duration of content that has expired from the
|
296 | * stream.
|
297 | *
|
298 | * @param {Object} playlist a media playlist object
|
299 | * dropped off the front of the playlist in a live scenario
|
300 | * @param {Number=} expired the amount of time that has
|
301 | * dropped off the front of the playlist in a live scenario
|
302 | * @return {TimeRanges} the periods of time that are valid targets
|
303 | * for seeking
|
304 | */
|
305 | var seekable = function seekable(playlist, expired) {
|
306 | var useSafeLiveEnd = true;
|
307 | var seekableStart = expired || 0;
|
308 | var seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd);
|
309 |
|
310 | if (seekableEnd === null) {
|
311 | return (0, _videoJs.createTimeRange)();
|
312 | }
|
313 | return (0, _videoJs.createTimeRange)(seekableStart, seekableEnd);
|
314 | };
|
315 |
|
316 | exports.seekable = seekable;
|
317 | var isWholeNumber = function isWholeNumber(num) {
|
318 | return num - Math.floor(num) === 0;
|
319 | };
|
320 |
|
321 | var roundSignificantDigit = function roundSignificantDigit(increment, num) {
|
322 | // If we have a whole number, just add 1 to it
|
323 | if (isWholeNumber(num)) {
|
324 | return num + increment * 0.1;
|
325 | }
|
326 |
|
327 | var numDecimalDigits = num.toString().split('.')[1].length;
|
328 |
|
329 | for (var i = 1; i <= numDecimalDigits; i++) {
|
330 | var scale = Math.pow(10, i);
|
331 | var temp = num * scale;
|
332 |
|
333 | if (isWholeNumber(temp) || i === numDecimalDigits) {
|
334 | return (temp + increment) / scale;
|
335 | }
|
336 | }
|
337 | };
|
338 |
|
339 | var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1);
|
340 | var floorLeastSignificantDigit = roundSignificantDigit.bind(null, -1);
|
341 |
|
342 | /**
|
343 | * Determine the index and estimated starting time of the segment that
|
344 | * contains a specified playback position in a media playlist.
|
345 | *
|
346 | * @param {Object} playlist the media playlist to query
|
347 | * @param {Number} currentTime The number of seconds since the earliest
|
348 | * possible position to determine the containing segment for
|
349 | * @param {Number} startIndex
|
350 | * @param {Number} startTime
|
351 | * @return {Object}
|
352 | */
|
353 | var getMediaInfoForTime = function getMediaInfoForTime(playlist, currentTime, startIndex, startTime) {
|
354 | var i = undefined;
|
355 | var segment = undefined;
|
356 | var numSegments = playlist.segments.length;
|
357 |
|
358 | var time = currentTime - startTime;
|
359 |
|
360 | if (time < 0) {
|
361 | // Walk backward from startIndex in the playlist, adding durations
|
362 | // until we find a segment that contains `time` and return it
|
363 | if (startIndex > 0) {
|
364 | for (i = startIndex - 1; i >= 0; i--) {
|
365 | segment = playlist.segments[i];
|
366 | time += floorLeastSignificantDigit(segment.duration);
|
367 | if (time > 0) {
|
368 | return {
|
369 | mediaIndex: i,
|
370 | startTime: startTime - sumDurations(playlist, startIndex, i)
|
371 | };
|
372 | }
|
373 | }
|
374 | }
|
375 | // We were unable to find a good segment within the playlist
|
376 | // so select the first segment
|
377 | return {
|
378 | mediaIndex: 0,
|
379 | startTime: currentTime
|
380 | };
|
381 | }
|
382 |
|
383 | // When startIndex is negative, we first walk forward to first segment
|
384 | // adding target durations. If we "run out of time" before getting to
|
385 | // the first segment, return the first segment
|
386 | if (startIndex < 0) {
|
387 | for (i = startIndex; i < 0; i++) {
|
388 | time -= playlist.targetDuration;
|
389 | if (time < 0) {
|
390 | return {
|
391 | mediaIndex: 0,
|
392 | startTime: currentTime
|
393 | };
|
394 | }
|
395 | }
|
396 | startIndex = 0;
|
397 | }
|
398 |
|
399 | // Walk forward from startIndex in the playlist, subtracting durations
|
400 | // until we find a segment that contains `time` and return it
|
401 | for (i = startIndex; i < numSegments; i++) {
|
402 | segment = playlist.segments[i];
|
403 | time -= ceilLeastSignificantDigit(segment.duration);
|
404 | if (time < 0) {
|
405 | return {
|
406 | mediaIndex: i,
|
407 | startTime: startTime + sumDurations(playlist, startIndex, i)
|
408 | };
|
409 | }
|
410 | }
|
411 |
|
412 | // We are out of possible candidates so load the last one...
|
413 | return {
|
414 | mediaIndex: numSegments - 1,
|
415 | startTime: currentTime
|
416 | };
|
417 | };
|
418 |
|
419 | exports.getMediaInfoForTime = getMediaInfoForTime;
|
420 | /**
|
421 | * Check whether the playlist is blacklisted or not.
|
422 | *
|
423 | * @param {Object} playlist the media playlist object
|
424 | * @return {boolean} whether the playlist is blacklisted or not
|
425 | * @function isBlacklisted
|
426 | */
|
427 | var isBlacklisted = function isBlacklisted(playlist) {
|
428 | return playlist.excludeUntil && playlist.excludeUntil > Date.now();
|
429 | };
|
430 |
|
431 | exports.isBlacklisted = isBlacklisted;
|
432 | /**
|
433 | * Check whether the playlist is compatible with current playback configuration or has
|
434 | * been blacklisted permanently for being incompatible.
|
435 | *
|
436 | * @param {Object} playlist the media playlist object
|
437 | * @return {boolean} whether the playlist is incompatible or not
|
438 | * @function isIncompatible
|
439 | */
|
440 | var isIncompatible = function isIncompatible(playlist) {
|
441 | return playlist.excludeUntil && playlist.excludeUntil === Infinity;
|
442 | };
|
443 |
|
444 | exports.isIncompatible = isIncompatible;
|
445 | /**
|
446 | * Check whether the playlist is enabled or not.
|
447 | *
|
448 | * @param {Object} playlist the media playlist object
|
449 | * @return {boolean} whether the playlist is enabled or not
|
450 | * @function isEnabled
|
451 | */
|
452 | var isEnabled = function isEnabled(playlist) {
|
453 | var blacklisted = isBlacklisted(playlist);
|
454 |
|
455 | return !playlist.disabled && !blacklisted;
|
456 | };
|
457 |
|
458 | exports.isEnabled = isEnabled;
|
459 | /**
|
460 | * Check whether the playlist has been manually disabled through the representations api.
|
461 | *
|
462 | * @param {Object} playlist the media playlist object
|
463 | * @return {boolean} whether the playlist is disabled manually or not
|
464 | * @function isDisabled
|
465 | */
|
466 | var isDisabled = function isDisabled(playlist) {
|
467 | return playlist.disabled;
|
468 | };
|
469 |
|
470 | exports.isDisabled = isDisabled;
|
471 | /**
|
472 | * Returns whether the current playlist is an AES encrypted HLS stream
|
473 | *
|
474 | * @return {Boolean} true if it's an AES encrypted HLS stream
|
475 | */
|
476 | var isAes = function isAes(media) {
|
477 | for (var i = 0; i < media.segments.length; i++) {
|
478 | if (media.segments[i].key) {
|
479 | return true;
|
480 | }
|
481 | }
|
482 | return false;
|
483 | };
|
484 |
|
485 | exports.isAes = isAes;
|
486 | /**
|
487 | * Returns whether the current playlist contains fMP4
|
488 | *
|
489 | * @return {Boolean} true if the playlist contains fMP4
|
490 | */
|
491 | var isFmp4 = function isFmp4(media) {
|
492 | for (var i = 0; i < media.segments.length; i++) {
|
493 | if (media.segments[i].map) {
|
494 | return true;
|
495 | }
|
496 | }
|
497 | return false;
|
498 | };
|
499 |
|
500 | exports.isFmp4 = isFmp4;
|
501 | /**
|
502 | * Checks if the playlist has a value for the specified attribute
|
503 | *
|
504 | * @param {String} attr
|
505 | * Attribute to check for
|
506 | * @param {Object} playlist
|
507 | * The media playlist object
|
508 | * @return {Boolean}
|
509 | * Whether the playlist contains a value for the attribute or not
|
510 | * @function hasAttribute
|
511 | */
|
512 | var hasAttribute = function hasAttribute(attr, playlist) {
|
513 | return playlist.attributes && playlist.attributes[attr];
|
514 | };
|
515 |
|
516 | exports.hasAttribute = hasAttribute;
|
517 | /**
|
518 | * Estimates the time required to complete a segment download from the specified playlist
|
519 | *
|
520 | * @param {Number} segmentDuration
|
521 | * Duration of requested segment
|
522 | * @param {Number} bandwidth
|
523 | * Current measured bandwidth of the player
|
524 | * @param {Object} playlist
|
525 | * The media playlist object
|
526 | * @param {Number=} bytesReceived
|
527 | * Number of bytes already received for the request. Defaults to 0
|
528 | * @return {Number|NaN}
|
529 | * The estimated time to request the segment. NaN if bandwidth information for
|
530 | * the given playlist is unavailable
|
531 | * @function estimateSegmentRequestTime
|
532 | */
|
533 | var estimateSegmentRequestTime = function estimateSegmentRequestTime(segmentDuration, bandwidth, playlist) {
|
534 | var bytesReceived = arguments.length <= 3 || arguments[3] === undefined ? 0 : arguments[3];
|
535 |
|
536 | if (!hasAttribute('BANDWIDTH', playlist)) {
|
537 | return NaN;
|
538 | }
|
539 |
|
540 | var size = segmentDuration * playlist.attributes.BANDWIDTH;
|
541 |
|
542 | return (size - bytesReceived * 8) / bandwidth;
|
543 | };
|
544 |
|
545 | exports.estimateSegmentRequestTime = estimateSegmentRequestTime;
|
546 | // exports
|
547 | exports['default'] = {
|
548 | duration: duration,
|
549 | seekable: seekable,
|
550 | safeLiveIndex: safeLiveIndex,
|
551 | getMediaInfoForTime: getMediaInfoForTime,
|
552 | isEnabled: isEnabled,
|
553 | isDisabled: isDisabled,
|
554 | isBlacklisted: isBlacklisted,
|
555 | isIncompatible: isIncompatible,
|
556 | playlistEnd: playlistEnd,
|
557 | isAes: isAes,
|
558 | isFmp4: isFmp4,
|
559 | hasAttribute: hasAttribute,
|
560 | estimateSegmentRequestTime: estimateSegmentRequestTime
|
561 | }; |
\ | No newline at end of file |