Home Reference Source

src/controller/cap-level-controller.js

  1. /*
  2. * cap stream level to media size dimension controller
  3. */
  4.  
  5. import Event from '../events';
  6. import EventHandler from '../event-handler';
  7.  
  8. class CapLevelController extends EventHandler {
  9. constructor (hls) {
  10. super(hls,
  11. Event.FPS_DROP_LEVEL_CAPPING,
  12. Event.MEDIA_ATTACHING,
  13. Event.MANIFEST_PARSED,
  14. Event.LEVELS_UPDATED,
  15. Event.BUFFER_CODECS,
  16. Event.MEDIA_DETACHING);
  17.  
  18. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  19. this.firstLevel = null;
  20. this.levels = [];
  21. this.media = null;
  22. this.restrictedLevels = [];
  23. this.timer = null;
  24. this.clientRect = null;
  25. }
  26.  
  27. destroy () {
  28. if (this.hls.config.capLevelToPlayerSize) {
  29. this.media = null;
  30. this.clientRect = null;
  31. this.stopCapping();
  32. }
  33. }
  34.  
  35. onFpsDropLevelCapping (data) {
  36. // Don't add a restricted level more than once
  37. if (CapLevelController.isLevelAllowed(data.droppedLevel, this.restrictedLevels)) {
  38. this.restrictedLevels.push(data.droppedLevel);
  39. }
  40. }
  41.  
  42. onMediaAttaching (data) {
  43. this.media = data.media instanceof window.HTMLVideoElement ? data.media : null;
  44. }
  45.  
  46. onManifestParsed (data) {
  47. const hls = this.hls;
  48. this.restrictedLevels = [];
  49. this.levels = data.levels;
  50. this.firstLevel = data.firstLevel;
  51. if (hls.config.capLevelToPlayerSize && data.video) {
  52. // Start capping immediately if the manifest has signaled video codecs
  53. this.startCapping();
  54. }
  55. }
  56.  
  57. // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted
  58. // to the first level
  59. onBufferCodecs (data) {
  60. const hls = this.hls;
  61. if (hls.config.capLevelToPlayerSize && data.video) {
  62. // If the manifest did not signal a video codec capping has been deferred until we're certain video is present
  63. this.startCapping();
  64. }
  65. }
  66.  
  67. onLevelsUpdated (data) {
  68. this.levels = data.levels;
  69. }
  70.  
  71. onMediaDetaching () {
  72. this.stopCapping();
  73. }
  74.  
  75. detectPlayerSize () {
  76. if (this.media) {
  77. let levelsLength = this.levels ? this.levels.length : 0;
  78. if (levelsLength) {
  79. const hls = this.hls;
  80. hls.autoLevelCapping = this.getMaxLevel(levelsLength - 1);
  81. if (hls.autoLevelCapping > this.autoLevelCapping) {
  82. // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
  83. // usually happen when the user go to the fullscreen mode.
  84. hls.streamController.nextLevelSwitch();
  85. }
  86. this.autoLevelCapping = hls.autoLevelCapping;
  87. }
  88. }
  89. }
  90.  
  91. /*
  92. * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
  93. */
  94. getMaxLevel (capLevelIndex) {
  95. if (!this.levels) {
  96. return -1;
  97. }
  98.  
  99. const validLevels = this.levels.filter((level, index) =>
  100. CapLevelController.isLevelAllowed(index, this.restrictedLevels) && index <= capLevelIndex
  101. );
  102.  
  103. this.clientRect = null;
  104. return CapLevelController.getMaxLevelByMediaSize(validLevels, this.mediaWidth, this.mediaHeight);
  105. }
  106.  
  107. startCapping () {
  108. if (this.timer) {
  109. // Don't reset capping if started twice; this can happen if the manifest signals a video codec
  110. return;
  111. }
  112. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  113. this.hls.firstLevel = this.getMaxLevel(this.firstLevel);
  114. clearInterval(this.timer);
  115. this.timer = setInterval(this.detectPlayerSize.bind(this), 1000);
  116. this.detectPlayerSize();
  117. }
  118.  
  119. stopCapping () {
  120. this.restrictedLevels = [];
  121. this.firstLevel = null;
  122. this.autoLevelCapping = Number.POSITIVE_INFINITY;
  123. if (this.timer) {
  124. this.timer = clearInterval(this.timer);
  125. this.timer = null;
  126. }
  127. }
  128.  
  129. getDimensions () {
  130. if (this.clientRect) {
  131. return this.clientRect;
  132. }
  133. const media = this.media;
  134. const boundsRect = {
  135. width: 0,
  136. height: 0
  137. };
  138.  
  139. if (media) {
  140. const clientRect = media.getBoundingClientRect();
  141. boundsRect.width = clientRect.width;
  142. boundsRect.height = clientRect.height;
  143. if (!boundsRect.width && !boundsRect.height) {
  144. // When the media element has no width or height (equivalent to not being in the DOM),
  145. // then use its width and height attributes (media.width, media.height)
  146. boundsRect.width = clientRect.right - clientRect.left || media.width || 0;
  147. boundsRect.height = clientRect.bottom - clientRect.top || media.height || 0;
  148. }
  149. }
  150. this.clientRect = boundsRect;
  151. return boundsRect;
  152. }
  153.  
  154. get mediaWidth () {
  155. return this.getDimensions().width * CapLevelController.contentScaleFactor;
  156. }
  157.  
  158. get mediaHeight () {
  159. return this.getDimensions().height * CapLevelController.contentScaleFactor;
  160. }
  161.  
  162. static get contentScaleFactor () {
  163. let pixelRatio = 1;
  164. try {
  165. pixelRatio = window.devicePixelRatio;
  166. } catch (e) {}
  167. return pixelRatio;
  168. }
  169.  
  170. static isLevelAllowed (level, restrictedLevels = []) {
  171. return restrictedLevels.indexOf(level) === -1;
  172. }
  173.  
  174. static getMaxLevelByMediaSize (levels, width, height) {
  175. if (!levels || (levels && !levels.length)) {
  176. return -1;
  177. }
  178.  
  179. // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next
  180. // to determine whether we've chosen the greatest bandwidth for the media's dimensions
  181. const atGreatestBandiwdth = (curLevel, nextLevel) => {
  182. if (!nextLevel) {
  183. return true;
  184. }
  185.  
  186. return curLevel.width !== nextLevel.width || curLevel.height !== nextLevel.height;
  187. };
  188.  
  189. // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to
  190. // the max level
  191. let maxLevelIndex = levels.length - 1;
  192.  
  193. for (let i = 0; i < levels.length; i += 1) {
  194. const level = levels[i];
  195. if ((level.width >= width || level.height >= height) && atGreatestBandiwdth(level, levels[i + 1])) {
  196. maxLevelIndex = i;
  197. break;
  198. }
  199. }
  200.  
  201. return maxLevelIndex;
  202. }
  203. }
  204.  
  205. export default CapLevelController;