UNPKG

15.7 kBJavaScriptView Raw
1/**
2 * @file playlist.js
3 *
4 * Playlist related utilities.
5 */
6'use strict';
7
8Object.defineProperty(exports, '__esModule', {
9 value: true
10});
11
12function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
13
14var _videoJs = require('video.js');
15
16var _globalWindow = require('global/window');
17
18var _globalWindow2 = _interopRequireDefault(_globalWindow);
19
20var 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
38var 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 */
80var 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 */
122var 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 */
171var 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
198exports.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 */
210var 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
233exports.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 */
247var 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
266exports.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 */
282var 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
293exports.seekable = seekable;
294var isWholeNumber = function isWholeNumber(num) {
295 return num - Math.floor(num) === 0;
296};
297
298var 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
316var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1);
317var 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 */
330var 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
396exports.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 */
404var isBlacklisted = function isBlacklisted(playlist) {
405 return playlist.excludeUntil && playlist.excludeUntil > Date.now();
406};
407
408exports.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 */
416var isEnabled = function isEnabled(playlist) {
417 var blacklisted = isBlacklisted(playlist);
418
419 return !playlist.disabled && !blacklisted;
420};
421
422exports.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 */
428var 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
437exports.isAes = isAes;
438/**
439 * Returns whether the current playlist contains fMP4
440 *
441 * @return {Boolean} true if the playlist contains fMP4
442 */
443var 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
452exports.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 */
464var hasAttribute = function hasAttribute(attr, playlist) {
465 return playlist.attributes && playlist.attributes[attr];
466};
467
468exports.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 */
485var 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
497exports.estimateSegmentRequestTime = estimateSegmentRequestTime;
498Playlist.duration = duration;
499Playlist.seekable = seekable;
500Playlist.getMediaInfoForTime = getMediaInfoForTime;
501Playlist.isEnabled = isEnabled;
502Playlist.isBlacklisted = isBlacklisted;
503Playlist.playlistEnd = playlistEnd;
504Playlist.isAes = isAes;
505Playlist.isFmp4 = isFmp4;
506Playlist.hasAttribute = hasAttribute;
507Playlist.estimateSegmentRequestTime = estimateSegmentRequestTime;
508
509// exports
510exports['default'] = Playlist;
\No newline at end of file