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