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