playlist.js 17 KB

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