playlist-selectors.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', {
  3. value: true
  4. });
  5. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
  6. var _config = require('./config');
  7. var _config2 = _interopRequireDefault(_config);
  8. var _playlist = require('./playlist');
  9. var _playlist2 = _interopRequireDefault(_playlist);
  10. var _utilCodecsJs = require('./util/codecs.js');
  11. // Utilities
  12. /**
  13. * Returns the CSS value for the specified property on an element
  14. * using `getComputedStyle`. Firefox has a long-standing issue where
  15. * getComputedStyle() may return null when running in an iframe with
  16. * `display: none`.
  17. *
  18. * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
  19. * @param {HTMLElement} el the htmlelement to work on
  20. * @param {string} the proprety to get the style for
  21. */
  22. var safeGetComputedStyle = function safeGetComputedStyle(el, property) {
  23. var result = undefined;
  24. if (!el) {
  25. return '';
  26. }
  27. result = window.getComputedStyle(el);
  28. if (!result) {
  29. return '';
  30. }
  31. return result[property];
  32. };
  33. /**
  34. * Resuable stable sort function
  35. *
  36. * @param {Playlists} array
  37. * @param {Function} sortFn Different comparators
  38. * @function stableSort
  39. */
  40. var stableSort = function stableSort(array, sortFn) {
  41. var newArray = array.slice();
  42. array.sort(function (left, right) {
  43. var cmp = sortFn(left, right);
  44. if (cmp === 0) {
  45. return newArray.indexOf(left) - newArray.indexOf(right);
  46. }
  47. return cmp;
  48. });
  49. };
  50. /**
  51. * A comparator function to sort two playlist object by bandwidth.
  52. *
  53. * @param {Object} left a media playlist object
  54. * @param {Object} right a media playlist object
  55. * @return {Number} Greater than zero if the bandwidth attribute of
  56. * left is greater than the corresponding attribute of right. Less
  57. * than zero if the bandwidth of right is greater than left and
  58. * exactly zero if the two are equal.
  59. */
  60. var comparePlaylistBandwidth = function comparePlaylistBandwidth(left, right) {
  61. var leftBandwidth = undefined;
  62. var rightBandwidth = undefined;
  63. if (left.attributes.BANDWIDTH) {
  64. leftBandwidth = left.attributes.BANDWIDTH;
  65. }
  66. leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
  67. if (right.attributes.BANDWIDTH) {
  68. rightBandwidth = right.attributes.BANDWIDTH;
  69. }
  70. rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
  71. return leftBandwidth - rightBandwidth;
  72. };
  73. exports.comparePlaylistBandwidth = comparePlaylistBandwidth;
  74. /**
  75. * A comparator function to sort two playlist object by resolution (width).
  76. * @param {Object} left a media playlist object
  77. * @param {Object} right a media playlist object
  78. * @return {Number} Greater than zero if the resolution.width attribute of
  79. * left is greater than the corresponding attribute of right. Less
  80. * than zero if the resolution.width of right is greater than left and
  81. * exactly zero if the two are equal.
  82. */
  83. var comparePlaylistResolution = function comparePlaylistResolution(left, right) {
  84. var leftWidth = undefined;
  85. var rightWidth = undefined;
  86. if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
  87. leftWidth = left.attributes.RESOLUTION.width;
  88. }
  89. leftWidth = leftWidth || window.Number.MAX_VALUE;
  90. if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
  91. rightWidth = right.attributes.RESOLUTION.width;
  92. }
  93. rightWidth = rightWidth || window.Number.MAX_VALUE;
  94. // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
  95. // have the same media dimensions/ resolution
  96. if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
  97. return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
  98. }
  99. return leftWidth - rightWidth;
  100. };
  101. exports.comparePlaylistResolution = comparePlaylistResolution;
  102. /**
  103. * Chooses the appropriate media playlist based on bandwidth and player size
  104. *
  105. * @param {Object} master
  106. * Object representation of the master manifest
  107. * @param {Number} playerBandwidth
  108. * Current calculated bandwidth of the player
  109. * @param {Number} playerWidth
  110. * Current width of the player element
  111. * @param {Number} playerHeight
  112. * Current height of the player element
  113. * @return {Playlist} the highest bitrate playlist less than the
  114. * currently detected bandwidth, accounting for some amount of
  115. * bandwidth variance
  116. */
  117. var simpleSelector = function simpleSelector(master, playerBandwidth, playerWidth, playerHeight) {
  118. // convert the playlists to an intermediary representation to make comparisons easier
  119. var sortedPlaylistReps = master.playlists.map(function (playlist) {
  120. var width = undefined;
  121. var height = undefined;
  122. var bandwidth = undefined;
  123. width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
  124. height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
  125. bandwidth = playlist.attributes.BANDWIDTH;
  126. bandwidth = bandwidth || window.Number.MAX_VALUE;
  127. return {
  128. bandwidth: bandwidth,
  129. width: width,
  130. height: height,
  131. playlist: playlist
  132. };
  133. });
  134. stableSort(sortedPlaylistReps, function (left, right) {
  135. return left.bandwidth - right.bandwidth;
  136. });
  137. // filter out any playlists that have been excluded due to
  138. // incompatible configurations
  139. sortedPlaylistReps = sortedPlaylistReps.filter(function (rep) {
  140. return !_playlist2['default'].isIncompatible(rep.playlist);
  141. });
  142. // filter out any playlists that have been disabled manually through the representations
  143. // api or blacklisted temporarily due to playback errors.
  144. var enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) {
  145. return _playlist2['default'].isEnabled(rep.playlist);
  146. });
  147. if (!enabledPlaylistReps.length) {
  148. // if there are no enabled playlists, then they have all been blacklisted or disabled
  149. // by the user through the representations api. In this case, ignore blacklisting and
  150. // fallback to what the user wants by using playlists the user has not disabled.
  151. enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) {
  152. return !_playlist2['default'].isDisabled(rep.playlist);
  153. });
  154. }
  155. // filter out any variant that has greater effective bitrate
  156. // than the current estimated bandwidth
  157. var bandwidthPlaylistReps = enabledPlaylistReps.filter(function (rep) {
  158. return rep.bandwidth * _config2['default'].BANDWIDTH_VARIANCE < playerBandwidth;
  159. });
  160. var highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1];
  161. // get all of the renditions with the same (highest) bandwidth
  162. // and then taking the very first element
  163. var bandwidthBestRep = bandwidthPlaylistReps.filter(function (rep) {
  164. return rep.bandwidth === highestRemainingBandwidthRep.bandwidth;
  165. })[0];
  166. // filter out playlists without resolution information
  167. var haveResolution = bandwidthPlaylistReps.filter(function (rep) {
  168. return rep.width && rep.height;
  169. });
  170. // sort variants by resolution
  171. stableSort(haveResolution, function (left, right) {
  172. return left.width - right.width;
  173. });
  174. // if we have the exact resolution as the player use it
  175. var resolutionBestRepList = haveResolution.filter(function (rep) {
  176. return rep.width === playerWidth && rep.height === playerHeight;
  177. });
  178. highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1];
  179. // ensure that we pick the highest bandwidth variant that have exact resolution
  180. var resolutionBestRep = resolutionBestRepList.filter(function (rep) {
  181. return rep.bandwidth === highestRemainingBandwidthRep.bandwidth;
  182. })[0];
  183. var resolutionPlusOneList = undefined;
  184. var resolutionPlusOneSmallest = undefined;
  185. var resolutionPlusOneRep = undefined;
  186. // find the smallest variant that is larger than the player
  187. // if there is no match of exact resolution
  188. if (!resolutionBestRep) {
  189. resolutionPlusOneList = haveResolution.filter(function (rep) {
  190. return rep.width > playerWidth || rep.height > playerHeight;
  191. });
  192. // find all the variants have the same smallest resolution
  193. resolutionPlusOneSmallest = resolutionPlusOneList.filter(function (rep) {
  194. return rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height;
  195. });
  196. // ensure that we also pick the highest bandwidth variant that
  197. // is just-larger-than the video player
  198. highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
  199. resolutionPlusOneRep = resolutionPlusOneSmallest.filter(function (rep) {
  200. return rep.bandwidth === highestRemainingBandwidthRep.bandwidth;
  201. })[0];
  202. }
  203. // fallback chain of variants
  204. var chosenRep = resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0];
  205. return chosenRep ? chosenRep.playlist : null;
  206. };
  207. exports.simpleSelector = simpleSelector;
  208. // Playlist Selectors
  209. /**
  210. * Chooses the appropriate media playlist based on the most recent
  211. * bandwidth estimate and the player size.
  212. *
  213. * Expects to be called within the context of an instance of HlsHandler
  214. *
  215. * @return {Playlist} the highest bitrate playlist less than the
  216. * currently detected bandwidth, accounting for some amount of
  217. * bandwidth variance
  218. */
  219. var lastBandwidthSelector = function lastBandwidthSelector() {
  220. return simpleSelector(this.playlists.master, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
  221. };
  222. exports.lastBandwidthSelector = lastBandwidthSelector;
  223. /**
  224. * Chooses the appropriate media playlist based on an
  225. * exponential-weighted moving average of the bandwidth after
  226. * filtering for player size.
  227. *
  228. * Expects to be called within the context of an instance of HlsHandler
  229. *
  230. * @param {Number} decay - a number between 0 and 1. Higher values of
  231. * this parameter will cause previous bandwidth estimates to lose
  232. * significance more quickly.
  233. * @return {Function} a function which can be invoked to create a new
  234. * playlist selector function.
  235. * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  236. */
  237. var movingAverageBandwidthSelector = function movingAverageBandwidthSelector(decay) {
  238. var average = -1;
  239. if (decay < 0 || decay > 1) {
  240. throw new Error('Moving average bandwidth decay must be between 0 and 1.');
  241. }
  242. return function () {
  243. if (average < 0) {
  244. average = this.systemBandwidth;
  245. }
  246. average = decay * this.systemBandwidth + (1 - decay) * average;
  247. return simpleSelector(this.playlists.master, average, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10));
  248. };
  249. };
  250. exports.movingAverageBandwidthSelector = movingAverageBandwidthSelector;
  251. /**
  252. * Chooses the appropriate media playlist based on the potential to rebuffer
  253. *
  254. * @param {Object} settings
  255. * Object of information required to use this selector
  256. * @param {Object} settings.master
  257. * Object representation of the master manifest
  258. * @param {Number} settings.currentTime
  259. * The current time of the player
  260. * @param {Number} settings.bandwidth
  261. * Current measured bandwidth
  262. * @param {Number} settings.duration
  263. * Duration of the media
  264. * @param {Number} settings.segmentDuration
  265. * Segment duration to be used in round trip time calculations
  266. * @param {Number} settings.timeUntilRebuffer
  267. * Time left in seconds until the player has to rebuffer
  268. * @param {Number} settings.currentTimeline
  269. * The current timeline segments are being loaded from
  270. * @param {SyncController} settings.syncController
  271. * SyncController for determining if we have a sync point for a given playlist
  272. * @return {Object|null}
  273. * {Object} return.playlist
  274. * The highest bandwidth playlist with the least amount of rebuffering
  275. * {Number} return.rebufferingImpact
  276. * The amount of time in seconds switching to this playlist will rebuffer. A
  277. * negative value means that switching will cause zero rebuffering.
  278. */
  279. var minRebufferMaxBandwidthSelector = function minRebufferMaxBandwidthSelector(settings) {
  280. var master = settings.master;
  281. var currentTime = settings.currentTime;
  282. var bandwidth = settings.bandwidth;
  283. var duration = settings.duration;
  284. var segmentDuration = settings.segmentDuration;
  285. var timeUntilRebuffer = settings.timeUntilRebuffer;
  286. var currentTimeline = settings.currentTimeline;
  287. var syncController = settings.syncController;
  288. // filter out any playlists that have been excluded due to
  289. // incompatible configurations
  290. var compatiblePlaylists = master.playlists.filter(function (playlist) {
  291. return !_playlist2['default'].isIncompatible(playlist);
  292. });
  293. // filter out any playlists that have been disabled manually through the representations
  294. // api or blacklisted temporarily due to playback errors.
  295. var enabledPlaylists = compatiblePlaylists.filter(_playlist2['default'].isEnabled);
  296. if (!enabledPlaylists.length) {
  297. // if there are no enabled playlists, then they have all been blacklisted or disabled
  298. // by the user through the representations api. In this case, ignore blacklisting and
  299. // fallback to what the user wants by using playlists the user has not disabled.
  300. enabledPlaylists = compatiblePlaylists.filter(function (playlist) {
  301. return !_playlist2['default'].isDisabled(playlist);
  302. });
  303. }
  304. var bandwidthPlaylists = enabledPlaylists.filter(_playlist2['default'].hasAttribute.bind(null, 'BANDWIDTH'));
  305. var rebufferingEstimates = bandwidthPlaylists.map(function (playlist) {
  306. var syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime);
  307. // If there is no sync point for this playlist, switching to it will require a
  308. // sync request first. This will double the request time
  309. var numRequests = syncPoint ? 1 : 2;
  310. var requestTimeEstimate = _playlist2['default'].estimateSegmentRequestTime(segmentDuration, bandwidth, playlist);
  311. var rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer;
  312. return {
  313. playlist: playlist,
  314. rebufferingImpact: rebufferingImpact
  315. };
  316. });
  317. var noRebufferingPlaylists = rebufferingEstimates.filter(function (estimate) {
  318. return estimate.rebufferingImpact <= 0;
  319. });
  320. // Sort by bandwidth DESC
  321. stableSort(noRebufferingPlaylists, function (a, b) {
  322. return comparePlaylistBandwidth(b.playlist, a.playlist);
  323. });
  324. if (noRebufferingPlaylists.length) {
  325. return noRebufferingPlaylists[0];
  326. }
  327. stableSort(rebufferingEstimates, function (a, b) {
  328. return a.rebufferingImpact - b.rebufferingImpact;
  329. });
  330. return rebufferingEstimates[0] || null;
  331. };
  332. exports.minRebufferMaxBandwidthSelector = minRebufferMaxBandwidthSelector;
  333. /**
  334. * Chooses the appropriate media playlist, which in this case is the lowest bitrate
  335. * one with video. If no renditions with video exist, return the lowest audio rendition.
  336. *
  337. * Expects to be called within the context of an instance of HlsHandler
  338. *
  339. * @return {Object|null}
  340. * {Object} return.playlist
  341. * The lowest bitrate playlist that contains a video codec. If no such rendition
  342. * exists pick the lowest audio rendition.
  343. */
  344. var lowestBitrateCompatibleVariantSelector = function lowestBitrateCompatibleVariantSelector() {
  345. // filter out any playlists that have been excluded due to
  346. // incompatible configurations or playback errors
  347. var playlists = this.playlists.master.playlists.filter(_playlist2['default'].isEnabled);
  348. // Sort ascending by bitrate
  349. stableSort(playlists, function (a, b) {
  350. return comparePlaylistBandwidth(a, b);
  351. });
  352. // Parse and assume that playlists with no video codec have no video
  353. // (this is not necessarily true, although it is generally true).
  354. //
  355. // If an entire manifest has no valid videos everything will get filtered
  356. // out.
  357. var playlistsWithVideo = playlists.filter(function (playlist) {
  358. return (0, _utilCodecsJs.parseCodecs)(playlist.attributes.CODECS).videoCodec;
  359. });
  360. return playlistsWithVideo[0] || null;
  361. };
  362. exports.lowestBitrateCompatibleVariantSelector = lowestBitrateCompatibleVariantSelector;